Index: main/waeup.kofa/branches/uli-py3/CHANGES.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/CHANGES.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/CHANGES.txt	(revision 16045)
@@ -0,0 +1,699 @@
+Changes
+*******
+
+1.6.1.dev0 (unreleased)
+=======================
+
+* Bugfix: If level_session of a studylevel object has changed,
+  coursetickets_catalog must be informed. Otherwise lecturers
+  don't see their students.
+
+* Implement `BatchProcessor.checkCreateRequirements` and
+  `BatchProcessor.checkUpdateRequirements` methods mainly
+  to protect course result lists of graduated student.
+
+* Do not allow course validation if no score has been entered.
+
+* Add `unlock_score` field to `CourseTicketProcessor`.
+
+* Add `UpdateCourseTicketsView`.
+
+* Don't show 'Base Data' header on slips if no form fields do
+  follow.
+
+* When adding course tickets, check if course has already been
+  passed at previous level.
+
+* Add `StudentTrimmedPaymentExporter` (requested by Uniben).
+
+* Add `AccommodationDisplayFormPage`.
+
+* Don't allow students to add former courses in course lists.
+
+* Use kofa-filterbox for filtering users when assigniing
+  local roles in academics section.
+
+* Add `waeup.local.ReportsOfficer` role.
+
+* Add `TrimmedDataExporter`.
+
+* Make `getUsers` customizable.
+
+* Add logging message to `releaseBedIfMaintenanceNotPaid`.
+
+* Provide components for file uploads in the applicants section.
+
+* Copy also files from applicants to students section.
+
+* Reset _curr_stud_id if student could not be created.
+
+* Add `TranscriptDataExporter` and reorganize exporter names.
+
+* Add `session_level`filter to `StudentStudyLevelExporter`.
+
+* Add LetterPDFCreator utility and use this pdf creator
+  for admission slips. letterhead_admission.jpg file
+  must exist in students/static.
+
+* Add application worklfow transition `reset6`.
+
+* Add `StudentOutstandingCoursesExporter`.
+
+* Sort lecturers on scores slip.
+
+* Don't show hint on pdf slips.
+
+* Make max passport picture size customizable.
+
+* Redirect to `ChangePasswordRequestPage` after successfully
+  adding an officer.
+
+* Add get_bursary_data webservice.
+
+* Implement bed selector.
+
+* Implement combi payments.
+
+* Make max file size customizable.
+
+* Add `score_editing_disabled` switch at course level. 
+
+* Take `DELETION_MARKER` into consideration when updating passwords.
+
+* Implement parents access to student data.
+
+* Do not print picture on application slip if application is without
+  passport picture.
+
+* Do not copy applicant_id into reg_number field after applicant creation.
+
+* Successful applicant payments do trigger 'approve' transition only
+  if applicant is in state 'started' and either p_category is 'application' or
+  applicant is special.
+
+* Allow to add applicants containers with a number instead of entrance year
+  in container code.
+
+* Add semester filter to Fix `CourseTicketExporter`. 
+
+* Add `FacultiesExportJobContainerSelectStudents` view to allow
+  bursary officers to export student data for a  small subset of students
+  by entering a list of student ids or matric numbers.
+
+* Add passport picture switch to applicants containers.
+
+* Use decimal.Decimal to counteract Python floating point limitation.
+
+* Add level 0 (None) to course levels.
+
+* Ease customization of selectable payment categories.
+
+* Make maximum number of files listed in `finished` subfolder
+  customizable.
+
+* Add attendance_sheet.pdf view.
+
+* Implement course result validation workflow for lecturers.
+
+* Add graduated students filter.
+
+* Backup deleted graduated student data somewhere else to ease graduated
+  student data migration.
+
+* Add `transcript_remark` field to study levels. Add page to allow
+  transcript officers editing the field during transcript processing.
+
+* Increase password strength. Officers are now required
+  to set a password which has at least 8 characters and which contains
+  at least one uppercase letter, one lowercase letter and one
+  digit. Stored insecure passwords are no longer accepted.
+  Officers with an insecure password can't login and are
+  redirected to the `ChangePasswordRequestPage` to request a
+  new password.
+
+* Add `AccommodationPaymentsExporter` which can be used by
+  accommodation officers.
+
+* Add ReleaseExpiredAllocationsActionButton which has previously
+  only been used by Uniben.
+
+* Fix get_student_payment_stats. Department codes are not
+  unique. Use its path instead.
+
+* Implement study level 0 (Level Zero) option for storing
+  orphaned course tickets (tickets without level assignment
+  or information). Add `ticket_session` field to `ICourseTicket`.
+
+* Implement new transcript processing workflow.
+
+* Always display applicant payments if they exist.
+
+* Allow `ApplicationsManager` to view statistics.
+
+* Add payment session filter to payment exporters.
+
+* Add `SessionPaymentsOverviewExporter`.
+
+* Rename `StudentPaymentsOverviewExporter`.
+
+* Fix `CourseTicketExporter`. Ticket level and session filter
+  did not work.
+
+* Add payment category filter to payment exporters.
+
+* Remove 'hidden' attribute from ordereddSelectionList which has
+  caused a malfunction in Internet Exporer.
+
+* Set ssl index URL in buildout.cfg
+
+* Use the 'new' recaptcha v2.
+
+* Allow lecturer role assignment also at department level.
+
+1.6 (2018-04-05)
+================
+
+* Add ticket level and session filter to `CourseTicketExporter`.
+
+* Add 'Student Record Creation' Report page.
+
+* Add `CreateStudents` permission and `StudentsCreator` role.
+
+* Do not allow uploading data with trailing whitespaces.
+
+* Do no longer hide the 'Create students' buttons but switch the portal to
+  maintenance mode to ensure that nobody can enter the portal during
+  record creation except for user 'admin'. This guarantees that record
+  creation is only started once.
+
+* Replace `gpa_boundaries` utility attribute by `GPABoundaries` method
+  which may return different values in custom packages depending on faculty,
+  department or certificate code (requested by AAU Ekpoma).
+
+* Allow `ApplicantOnlinePaymentProcessor` to import records without
+  `p_id` column in create mode.
+
+* Add `waeup.showStudents` permission to `ExportManager` role.
+
+* Fix `CertificateProcessor.checkHeaders`. The processor requires both
+  faculty_code *and* department_code in create mode.
+
+* Add 'Update all tickets' button to `StudyLevelEditFormPage`.
+
+* Do not allow to create more than 10 students with a single request to
+  avoid a timeout of Nginx/Apache.
+
+* Redirect to manage page after editing of certificate courses.
+
+* Add `course_category` attribute to course tickets. In the base package,
+  this attribute is omitted from display and manage pages.
+
+* Add `course_category` attribute to certificate courses.
+
+* Simplify configuration of maximum or minimum total credits.
+
+* Add `outstanding` attribute to course tickets.
+
+* Exclude graduated students from `LevelReport`, `SessionResultsPresentation`
+  and `RawScoreReport`.
+
+* Allow applicants in states `admitted`, `not admitted` or `created` to
+  download the payment slip.
+
+* Catch traceback (`RequiredMissing`) if a required field is cleared
+  by import.
+
+* Add role `FingerprintReaderDeviceRole` intended for fingerprint reader
+  devices that access the portal via XMLRPC.
+
+
+1.5 (2017-02-14)
+================
+
+* Happy Valentines Day everybody!
+
+* Sort arguments in table of `ReportsContainerPage`.
+
+* Add `entry_session` and `p_session` filter to Student Payment Statistics
+  generator. Adjust reports: Change footer and filename formats.
+
+* Add officer name fields do `IDepartment` and `IFaculty`.
+  Plugins must be updated!
+
+* Add serial number column to Session Results Presentations.
+
+* Add Raw Score Report to base package.
+
+* Only user 'admin' is allowed to access the `DatacenterSettings` page.
+
+* Show correct level title on Level Reports and Session Results Presentations.
+
+* Get set of selected students also from list of matric numbers.
+
+* Confine Level Reports and Session Results Presentations to certificates.
+
+* Show report number (job_id) on report pdf slips.
+
+* Level report: Count and display the number of courses registered but
+  not taken.
+
+* Add control button to create graduated students.
+
+* Add option to render transcripts without passport picture and QR code.
+
+* Catch Reportlab `LayoutError`.
+
+* Replace `course_registration_allowed` by `course_registration_forbidden`
+  method.
+
+* Add export purge button.
+
+* Add report purge button.
+
+* Add property attribute `total_score` in order to make provision
+  for additional scores (like contineous assessments) in custom
+  packages.
+
+* Make provision against storing other objects than applicant payments
+  in applicant containers.
+
+* Count mandates on configuration page and provide 'Purge' button.
+
+* Start `ApplicantExporter` and `ApplicantPaymentExporter` at the same
+  time when calling `ExportJobContainerJobStart`.
+
+* Add `ApplicantPaymentExporter`.
+
+* Add file upload functionality to `EditScoresPage`.
+
+* Add `LecturerLandingPage` and `LecturerCourses` `ManageLink`
+  viewlet.
+
+* Add `ExportPDFScoresSlip`.
+
+* Add `DownloadScoresView` which is for lecturers only. Remove
+  `waeup.showStudents` from `Lecturer` role. Adjust
+  `DataForLecturerExporter`. Only editable course tickets are being
+  exported.
+
+* Remove application fee fallback option. Application fees can only
+  be set via applicants containers. Hide Payment Tickets section on
+  application pages if fee isn't set.
+
+* Add  `ApplicantOnlinePaymentProcessor`.
+
+* Improve `AddStudyLevelFormPage`.
+
+* Create new exporter for lecturers and further restrict the usage of
+  exporters at course level. Lecturers can only export data of
+  current academic session.
+
+* Show certificate-specific p_level value on payment pages.
+  Ensure that only allowed payment levels can be selected when
+  adding a balance payment.
+
+* Add `flash_notice` field.
+
+* Documentation updates.
+
+* Update pinned versions: six
+
+
+1.4.1 (2016-02-14)
+==================
+
+* Add new application workflow state (`processed`) and transition
+  (`process`).
+
+* Add `DegreeSource` and `degree` field to `ICertificate`.
+  Plugins must be updated!
+
+* Upgrade bootstrap.py script and update some dependencies. For
+  already running instances a new init might be neccessary, i.e.
+  you might have to create a new virtualenv and run bootstap.py
+  and buildout from that new virtualenv to get rid of old
+  package versions in the runtime environment.
+
+* Upgrade versions.cfg to contain bugfix releases of packages we
+  depend on.
+
+
+1.4 (2016-01-14)
+================
+
+* Add button and view which allow students to unregister their
+  current course list unless the courses have not been validated.
+
+* Configure transfer payments and let students enter their desired
+  study course. Save entered text in `p_item` attribute.
+
+* Add further permissions to the local `ApplicationsManager` role and
+  make it assignable at applicants container level.
+
+* Disable rendering of HTML tags in fullnames.
+
+* Add bed statistics components.
+
+* Add option which allows students to select a desired hostel before
+  booking accommodation.
+
+* Add `BedProcessor`.
+
+* Add portal maintenance mode.
+
+* Allow also `reg_number` on public page to check application status
+  without password.
+
+* Remove `beds_reserved` attribute from hostels. Hostels do no longer
+  contain information about reserved bed spaces.
+
+* Add methods and page to release expired bed allocations.
+
+* Use lastname instead of firstname for first-time password requests.
+
+* When building docs, ignore members with dots in name.
+
+
+1.3.3 (2015-10-06)
+==================
+
+* Show file size of pdf documents in the documents section.
+
+* Add `MANIFEST.in`.
+
+
+1.3.2 (2015-09-14)
+==================
+
+* Add public page to check application status without password.
+
+* Fix department breakdown of student statistics.
+
+* Enable purging of applicants containers.
+
+* Enable pre-filling of applicants containers with empty
+  application records which are used during self-registration
+  to avoid excessive growth of the Data.fs.
+
+* Do only allow one running export job.
+
+* Add switch to disable all exports.
+
+* Enable import of list-of-choices fields.
+
+* Include 'end day' when searching for payments in a given period.
+
+* Do not create special application payment tickets with zero amount.
+
+* Remove manual clearance lock. Replace `clearance_locked` field by
+  property attribute.
+
+* Make student email field required.
+
+* Rename application workflow transitions and remove forbidden transitions.
+
+* Rename 'application section' for the sake of consistency.
+
+* Rename student views which are not layout-aware and thus not pages.
+
+* Register KofaSourceOrderedMultiSelectWidget for IterableSource and
+  KofaSourceOrderedMultiSelectWidget for IVocabularyTokenized.
+
+* Implement session-specific course registration deadline and
+  late registration payment.
+
+* Remove quite old bug in `doImport`: Replace empty strings *and* lists with
+  ignore-markers in update *and* create mode.
+
+* Add StudentUnpaidPaymentExporter to export only unpaid tickets.
+  This exporter is designed for finding and finally purging outdated
+  payment ticket.
+
+* Remove deprecated xml importer and exporter components.
+
+* Rearrange datacenter upload page. Provide modal windows to view all
+  processors and sources and vocabularies.
+
+* Reorganize doctests. Remove deprecated doctests.
+
+* Enable temporary suspension of officer accounts. Plugins must be
+  updated after restart.
+
+* Rename 'Portal Users' 'Officers'.
+
+* Add link 'Kofa Docs for this page' at the end of the content box
+  which refers to the corresponding section of the
+  `Kofa Documentation <http://kofa-doc.waeup.org>`_.
+
+* Filter students by current level in reports.
+
+* Add `StudentsUtils.increaseMatricInteger` method which allows to use various
+  matric number counters when computing the matric number.
+
+* Bypass school fee payment activation code creation if student is allowed
+  to proceed to next session.
+
+* Convert `level` into a schema field to be consistent with the documentation.
+
+* Rename classes `StudentPaymentsExporter` and `BedTicketsExporter` to be
+  in accordance with other exporter names.
+
+* Add `ReportsOfficer` and `ReportsManager` roles with corresponding
+  permissions.
+
+* Do not automatically allow import managers to import user data.
+
+* Skip empty rows produced by spreadsheet software.
+
+* Add components to  configure a student export jobs in faculties.
+
+* `StudentOnlinePaymentProcessor` : Raise `DuplicationError` if payment
+  (in state ``paid``) with same `p_session` and `p_category` exists.
+
+* Add new payment state ``waived``. This state can only be set by import
+  and is only used for the payments overview exporter to
+  mark sessions with waived fees.
+
+* Add exporter methods and export page to filter student data exports
+  by entering a list of student ids.
+
+* Student statistic can now be broken down by faculties or by departments.
+
+* Check if `p_id` exists in payments catalog when importing payment tickets in
+  create mode.
+
+
+1.3.1 (2015-01-14)
+==================
+
+* 1.3 was a brown bag release. Fix uploaded files.
+
+
+1.3 (2015-01-13)
+================
+
+* Add documents package.
+
+* Do not compare ResultEntry objects but their attributes.
+
+* Modify getWidgetsData so that we always get the same error format.
+
+* Validate all codes in university.
+
+* Catch traceback when creating an application slip with a corrupted
+  image file.
+
+* Remove deprecated HTML and REST widgets and use html2dict and rest2dict
+  instead.
+
+* UserProcessor: Check if imported roles do exist in the portal.
+
+* Enable localization of batch processing modules.
+
+* Hide 'Create students' button. Only user admin can see this button.
+
+* Add application_slip_notice field to ApplicantsContainer.
+
+* Remove 'Size' column from logfiles table.
+
+* Add permission, page and button to mass-clear all students in a department.
+
+* Hide export download button 24 hours after file generation.
+
+* Add helper to allow `meminfo` checks for cached memory, etc.
+
+* Allow external checks of system load before performing expensive
+  actions.
+
+* Add new dependency: `psutil` (required to check system state).
+
+* Show flash message on general search page for the academic section.
+
+* Fix `CertificateCourseProcessor`. Mandatory is not a required field
+  and might be missing in import files. Improve logging. Extend and
+  fix CertCourseProcessorTests.
+
+* Provide customizable method for disabling clearance by clearance
+  officers.
+
+* Merge Javascript functions into one file. Add Javascript function to
+  calculate copyright notice year automatically.
+
+* Use lastname (surname) instead of firstname to verify found
+  application record on ApplicantRegistrationPage.
+
+* Append hash '#' also to datetimes to circumvent unwanted excel
+  automatic.
+
+* Method for (customizable) application passport upload requirements
+  added.
+
+* Filter payment ticket data exports by specifying the payment_date
+  period.
+
+* Method for (customizable) application payment slip download
+  requirements added.
+
+* Method for (customizable) application payment ticket upload
+  requirements added.
+
+* Remove unused rowadd javascript function.
+
+* certcode and reg_number added to DataForBursaryExporter.
+
+* Some batch processor names corrected.
+
+* Fixed ReST markup in setup.py long description.
+
+
+1.2 (2014-06-16)
+================
+
+* Redirect to login page after using password mandate.
+
+* Adding student payment tickets is logged.
+
+* Provide XMLRPC services for storing and retrieving fingerprint
+  scans.
+
+* Sort ContextualDictSource by dictionary values not by keys.
+
+* Add regular school fee and balance payments when exporting payments
+  overviews.
+
+* Add samePaymentMade method to StudentsUtils.
+
+* Fingerprint file upload enabled.
+
+* is_fresh property method added to Student class.
+
+* Special application procedure modified. Special applicants must submit
+  their form before they can download payment slips. Thus the regular
+  workflow is now applied but, in contrast to regular applications,
+  special applicants can create and pay more than one payment ticket.
+  Also application slips are hidden.
+
+* Add setMatricNumber and constructMatricNumber methods to StudentsUtils.
+
+* When sending mails, always set from-address to 'no-reply@waeup.org'.
+  Real from-addresses are now set in Cc and Reply-To fields. This is
+  to tackle problems with yahoo (does not allow yahoo.com mails to be
+  sent from non-yahoo servers).
+
+* Multiple payments in special application containers enabled.
+  Special payment applicants remain in state started after payment.
+
+* File input fields brought into shape with Bootstrap 3.
+
+* Transcript and access code pagetemplates adjusted.
+
+* Print QR codes on all student slips.
+
+* Allow adding current_level to base data section on all student slips
+  (requested by Uniben).
+
+* Windows is no longer supported. Installation manual removed.
+
+* Added JS helper to scroll to sequencelists after adding/removing items.
+
+
+1.1 (2014-03-13)
+================
+
+* Add display_fullname column to `CourseTicketExporter` (requested by Uniben).
+
+* Do also export student and course ticket data of students on probation.
+
+* 'Create student' option removed from 'Application Transition'
+  select field.
+
+* API docs added.
+
+* Back-to links replaced by animated jQuery plugins.
+
+* Default frontpage text updated.
+
+* Use absolute urls on datacenter pages.
+
+* Provide customizable method for disabling student payments.
+
+* After login and logout, regular users (officers) are redirected
+  to the portal's index page not to the portal's root. The
+  root URL can now be rewritten be Nginx or Apache so that the user
+  is being redirected to the university website
+  when entering the root URL directly or clicking the acronym.
+
+* Show tooltip alert and warning window before updating plugins.
+
+* JSAction has been deprecated. KofaAction can be used instead. Additional
+  parameters can be used in @action decorators to enable tooltips and/or
+  warning windows. The old @jsaction decorator still triggers a default
+  warning message.
+
+* jquery-ui.min.js is also used for tooltips.
+
+* Do not create TSC AC batch when updating plugins.
+  All TSC batches are created.
+
+* Make table on applicantsrootpage responsive.
+
+* Change width of input, select and textarea fields.
+
+* Use only https in theme.html.
+
+* INSTALL.txt updated and integrated into Sphinx documentation.
+
+* Redundancies removed in docs.
+
+
+1.0 (2014-02-22)
+================
+
+* Diazofied the complete package.
+
+
+0.2.1 (2014-02-21)
+==================
+
+* Send mails to from-address, if site-wide config admin is
+  different from it.
+
+* Change flash message for successful applicants.
+
+* Use more attributes of applicants with payer objects.
+
+* Update copyright infos (year).
+
+* Localization updates.
+
+
+0.2 (2014-01-09)
+================
+
+Tons of changes.
+
+
+0.1 (never released)
+====================
+
+Initial version. Copy from waeup.sirp.
Index: main/waeup.kofa/branches/uli-py3/LICENSE
===================================================================
--- main/waeup.kofa/branches/uli-py3/LICENSE	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/LICENSE	(revision 16045)
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 3 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
Index: main/waeup.kofa/branches/uli-py3/MANIFEST.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/MANIFEST.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/MANIFEST.in	(revision 16045)
@@ -0,0 +1,10 @@
+# see:
+# https://docs.python.org/2/distutils/sourcedist.html#the-manifest-in-template
+graft src
+graft docs
+prune docs/build
+recursive-include etc *.in *.txt
+graft layout
+include *.cfg bootstrap.py CHANGES.txt LICENSE
+exclude .* uuid.txt
+global-exclude *~ *.pyc *.pyo
Index: main/waeup.kofa/branches/uli-py3/README.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/README.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/README.txt	(revision 16045)
@@ -0,0 +1,78 @@
+What is WAeUP.Kofa ?
+********************
+
+Kofa means 'entrance' or 'portal' in Hausa language.
+
+WAeUP.Kofa is a multifunctional, open source, web based student
+management system to provide transparent and comprehensive
+information about application and study progress. The system
+controls all kinds of application and student registration processes
+at large schools and universities in Nigeria. It is a generic system
+which can be deployed by, and is useful to most universities,
+colleges or schools worldwide.
+
+WAeUP.Kofa is the flagship of the West African eUniversity Project
+(WAeUP). See http://www.waeup.org to learn more about WAeUP.
+
+WAeUP.Kofa is really an allrounder. The best way to briefly describe
+its functionality, is to explain what the software can *not* do for
+you, rather than trying to describe all its features in just a few
+words. Kofa is primarily not an e-learning system, although, it has
+some basic content management features which can be used by
+lecturers to disseminate course material. Kofa is also not a
+scheduler which generates lesson plans or timetables for lecturers
+or students. Although students register their courses with Kofa and
+can see or print course lists for each academic session, so far,
+courses do not contain information, when or where they take place.
+However, due to the modular design of Kofa, a scheduler or room
+planner could be easily added.
+
+WAeUP.Kofa is divided into sections. Each section has its own folder
+in Kofa's `object database`. When starting Kofa in debug mode::
+
+  $ ./bin/kofactl debug
+
+we can use simple Python expression to view the first two levels of
+the database structure::
+
+  >>> list(root.keys())
+  [u'app']
+  >>> list(root['app'].keys())
+  [u'accesscodes', u'applicants', u'configuration', u'datacenter',
+   u'documents', u'faculties', u'hostels', u'mandates', u'reports',
+   u'students', u'users']
+
+
+The section/folder structure can be figured as follows::
+
+  root (Database Root)
+  |
+  +---> app (University)
+        |
+        +---> faculties (Academic Section)
+        |
+        +---> students (Students Section)
+        |
+        +---> applicants (Aplicants Section)
+        |
+        +---> documents (Documents Section)
+        |
+        +---> hostels (Accommodation Section)
+        |
+        +---> accesscodes (Access Codes Section)
+        |
+        +---> configuration (Configuration Section)
+        |
+        +---> users (Officers)
+        |
+        +---> datacenter (Data Center)
+        |
+        +---> mandates (Mandates)
+        |
+        +---> reports (Reports)
+
+The user handbook follows this organizational structure and adds
+further chapters where needed. Also installation instructions and
+basic information about customization can be found there.
+
+You can also find latest docs at https://kofa-doc.waeup.org/
Index: main/waeup.kofa/branches/uli-py3/bootstrap.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/bootstrap.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/bootstrap.py	(revision 16045)
@@ -0,0 +1,210 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+__version__ = '2015-07-01'
+# See zc.buildout's changelog if this version is up to date.
+
+tmpeggs = tempfile.mkdtemp(prefix='bootstrap-')
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("--version",
+                  action="store_true", default=False,
+                  help=("Return bootstrap.py version."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+                  help=("Specify the path to the buildout configuration "
+                        "file to be used."))
+parser.add_option("-f", "--find-links",
+                  help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+                  action="store_true", default=False,
+                  help=("Let bootstrap.py use existing site packages"))
+parser.add_option("--buildout-version",
+                  help="Use a specific zc.buildout version")
+parser.add_option("--setuptools-version",
+                  help="Use a specific setuptools version")
+parser.add_option("--setuptools-to-dir",
+                  help=("Allow for re-use of existing directory of "
+                        "setuptools versions"))
+
+options, args = parser.parse_args()
+if options.version:
+    print("bootstrap.py version %s" % __version__)
+    sys.exit(0)
+
+
+######################################################################
+# load/install setuptools
+
+try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+ez = {}
+if os.path.exists('ez_setup.py'):
+    exec(open('ez_setup.py').read(), ez)
+else:
+    exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+    # ez_setup imports site, which adds site packages
+    # this will remove them from the path to ensure that incompatible versions
+    # of setuptools are not in the path
+    import site
+    # inside a virtualenv, there is no 'getsitepackages'.
+    # We can't remove these reliably
+    if hasattr(site, 'getsitepackages'):
+        for sitepackage_path in site.getsitepackages():
+            # Strip all site-packages directories from sys.path that
+            # are not sys.prefix; this is because on Windows
+            # sys.prefix is a site-package directory.
+            if sitepackage_path != sys.prefix:
+                sys.path[:] = [x for x in sys.path
+                               if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+
+if options.setuptools_version is not None:
+    setup_args['version'] = options.setuptools_version
+if options.setuptools_to_dir is not None:
+    setup_args['to_dir'] = options.setuptools_to_dir
+
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set.  We will
+# do it.
+for path in sys.path:
+    if path not in pkg_resources.working_set.entries:
+        pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+setuptools_path = ws.find(
+    pkg_resources.Requirement.parse('setuptools')).location
+
+# Fix sys.path here as easy_install.pth added before PYTHONPATH
+cmd = [sys.executable, '-c',
+       'import sys; sys.path[0:0] = [%r]; ' % setuptools_path +
+       'from setuptools.command.easy_install import main; main()',
+       '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+    'bootstrap-testing-find-links',
+    options.find_links or
+    ('http://downloads.buildout.org/'
+     if options.accept_buildout_test_releases else None)
+    )
+if find_links:
+    cmd.extend(['-f', find_links])
+
+requirement = 'zc.buildout'
+version = options.buildout_version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+
+    def _final_version(parsed_version):
+        try:
+            return not parsed_version.is_prerelease
+        except AttributeError:
+            # Older setuptools
+            for part in parsed_version:
+                if (part[:1] == '*') and (part not in _final_parts):
+                    return False
+            return True
+
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setuptools_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd) != 0:
+    raise Exception(
+        "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+    args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
Index: main/waeup.kofa/branches/uli-py3/buildout-zeo.cfg
===================================================================
--- main/waeup.kofa/branches/uli-py3/buildout-zeo.cfg	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/buildout-zeo.cfg	(revision 16045)
@@ -0,0 +1,131 @@
+# A buildout configuration for a ZEO server and two ZEO clients.
+#
+[buildout]
+extends = buildout.cfg
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    raw_deploy_ini
+    raw_debug_ini
+    themed_deploy_ini
+    themed_debug_ini
+    profile_ini
+    zeo1_ini
+    zeo2_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# kofactl creation must be _after_ app!
+    kofactl
+    lxml
+    diazo
+# For backward compatibility, telling buildout not to throw away
+# the data and log subdirectories from the parts directory.
+    data
+    log
+
+    zeo_server
+    zope_conf_zeo_1
+    zope_conf_zeo_2
+    zeo1_ini
+    zeo2_ini
+    zeo1_conf
+    zeo2_conf
+    zeo_client1
+    zeo_client2
+
+[zeo_server]
+recipe = zc.zodbrecipes:server
+zeo.conf =
+  <zeo>
+    address ${kofa_params:server_port}
+  </zeo>
+  <blobstorage 1>
+    blob-dir ${zope_conf:blobstorage}
+    <filestorage 1>
+      path ${zope_conf:filestorage}/Data.fs
+    </filestorage>
+  </blobstorage>
+  <filestorage async>
+    path ${zope_conf:filestorage}/Data.async.fs
+  </filestorage>
+  <eventlog>
+    # This sets up logging to a file.
+    # The "path" setting can be a relative or absolute
+    # filesystem path.
+    <logfile>
+      path ${zope_conf:logfiles}/z3.log
+      formatter zope.exceptions.log.Formatter
+    </logfile>
+    # This sets up logging to to standard output.
+    # The "path" setting can be the tokens STDOUT or STDERR
+    #  <logfile>
+    #    path STDOUT
+    #    formatter zope.exceptions.log.Formatter
+    #  </logfile>
+  </eventlog>
+
+[zope_conf_zeo_1]
+recipe = collective.recipe.template
+input = etc/zope_zeo1.conf.in
+output = ${buildout:parts-directory}/etc/zope_zeo1.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+[zope_conf_zeo_2]
+recipe = collective.recipe.template
+input = etc/zope_zeo2.conf.in
+output = ${buildout:parts-directory}/etc/zope_zeo2.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+[zeo1_ini]
+recipe = collective.recipe.template
+input = etc/themed/zeo1.ini.in
+output = ${buildout:parts-directory}/etc/themed-zeo1.ini
+
+[zeo2_ini]
+recipe = collective.recipe.template
+input = etc/themed/zeo2.ini.in
+output = ${buildout:parts-directory}/etc/themed-zeo2.ini
+
+[zeo1_conf]
+recipe = collective.recipe.template
+input = etc/zeo1.conf.in
+output = ${buildout:parts-directory}/etc/zeo1.conf
+
+[zeo2_conf]
+recipe = collective.recipe.template
+input = etc/zeo2.conf.in
+output = ${buildout:parts-directory}/etc/zeo2.conf
+
+
+[zeo_client1]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+recipe = zc.recipe.egg
+eggs = waeup.kofa [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zeo1.conf"
+scripts = kofactl=zeo_client1
+
+[zeo_client2]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+recipe = zc.recipe.egg
+eggs = waeup.kofa [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zeo2.conf"
+scripts = kofactl=zeo_client2
+
Index: main/waeup.kofa/branches/uli-py3/buildout.cfg
===================================================================
--- main/waeup.kofa/branches/uli-py3/buildout.cfg	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/buildout.cfg	(revision 16045)
@@ -0,0 +1,217 @@
+[buildout]
+extensions = mr.developer
+auto-checkout = 
+sources-dir = sources
+develop = .
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    raw_deploy_ini
+    raw_debug_ini
+    themed_deploy_ini
+    themed_debug_ini
+    profile_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# kofactl creation must be _after_ app!
+    kofactl
+    lxml
+    diazo
+# For backward compatibility, telling buildout not to throw away
+# the data and log subdirectories from the parts directory.
+    data
+    log
+# newest = false
+
+extends = versions.cfg
+show-picked-versions = true
+update-versions-file = versions.cfg
+
+index = https://pypi.python.org/simple/
+
+# eggs will be installed in the default buildout location
+# (see .buildout/default.cfg in your home directory)
+# unless you specify an eggs-directory option here.
+
+[sources]
+# None, currently.
+
+[kofa_params]
+## Basic parameters for deployment.
+# default is 127.0.0.1
+host = 0.0.0.0
+# default is 8080
+base_port = 8080
+zeo1_port = 8080
+zeo2_port = 8081
+zeo3_port = 8082
+server_port = 8100
+# default 7. Number of pre-opened ZODB threads/client
+pool_size = 7
+# default 20MB. Cache size of each client (ZEO only)
+client_cache = 20MB
+# default 5000. Number of objects cached.
+object_cache = 5000
+# number of preopened paster threads, default 10
+threadpool_workers = 10
+# package to be developed
+devel_pkg = waeup.kofa
+
+[app]
+# This creates all scripts in bin/. The kofactl created here is 'faulty'.
+recipe = zc.recipe.egg
+eggs = waeup.kofa [beaker, diazo]
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+       repoze.profile
+       ZODB3
+interpreter = python-console
+
+[kofactl]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+# Here we create the kofactl script again, this time with a
+# fixed path to zdaemon.conf as argument.
+recipe = zc.recipe.egg
+eggs = waeup.kofa [beaker, diazo]
+arguments = "${buildout:parts-directory}/etc/zdaemon.conf"
+scripts = kofactl=kofactl
+
+[mkdirs]
+recipe = z3c.recipe.mkdir
+paths =
+    ${zope_conf:filestorage}
+    ${zope_conf:logfiles}
+    ${zope_conf:blobstorage}
+    ${buildout:directory}/var/mailqueue/cur
+    ${buildout:directory}/var/mailqueue/tmp
+    ${buildout:directory}/var/mailqueue/new
+    ${buildout:directory}/var/datacenter/media
+    ${buildout:directory}/parts/test/datacenter
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = waeup.kofa [beaker, test]
+defaults = ['--tests-pattern', '^f?tests$', '-v']
+
+# this section named so that the i18n scripts are called bin/i18n...
+[i18n]
+recipe = z3c.recipe.i18n:i18n
+packages = waeup.kofa
+eggs = waeup.kofa
+domain = waeup.kofa
+output = src/waeup/kofa/locales
+zcml =
+
+# This section is named so that the zpasswd utility is
+# called `zpasswd`
+[zpasswd]
+recipe = z3c.recipe.dev:script
+eggs = waeup.kofa
+module = zope.app.server.zpasswd
+method = main
+
+[zope_conf]
+recipe = collective.recipe.template
+input = etc/zope.conf.in
+output = ${buildout:parts-directory}/etc/zope.conf
+filestorage = ${buildout:directory}/var/filestorage
+blobstorage = ${buildout:directory}/var/blobstorage
+logfiles = ${buildout:directory}/var/log
+extra =
+# extra = <grokwarnings>True</grokwarnings>
+# 'extra' is copied verbatim. Use it for product config sections and so.
+
+# The [data] and [log] parts are still in here to instruct buildout to not
+# unintentionally throw away the parts/data and parts/log subdirectories
+# that contain the Data.fs and the log files. These files should be
+# copied to the new locations. See the upgrade notes for more information.
+[data]
+recipe = zc.recipe.filestorage
+
+[log]
+recipe = zc.recipe.filestorage
+
+[mailqueue]
+recipe = zc.recipe.filestorage
+
+[site_zcml]
+recipe = collective.recipe.template
+input = etc/site.zcml.in
+output = ${buildout:parts-directory}/etc/site.zcml
+
+[zdaemon_conf]
+recipe = collective.recipe.template
+input = etc/zdaemon.conf.in
+output = ${buildout:parts-directory}/etc/zdaemon.conf
+
+[raw_debug_ini]
+recipe = collective.recipe.template
+input = etc/raw/debug.ini.in
+output = ${buildout:parts-directory}/etc/raw-debug.ini
+
+[raw_deploy_ini]
+recipe = collective.recipe.template
+input = etc/raw/deploy.ini.in
+output = ${buildout:parts-directory}/etc/raw-deploy.ini
+
+[themed_debug_ini]
+recipe = collective.recipe.template
+input = etc/themed/debug.ini.in
+output = ${buildout:parts-directory}/etc/themed-debug.ini
+
+[themed_deploy_ini]
+recipe = collective.recipe.template
+input = etc/themed/deploy.ini.in
+output = ${buildout:parts-directory}/etc/themed-deploy.ini
+
+[profile_ini]
+recipe = collective.recipe.template
+input = etc/profile.ini.in
+output = ${buildout:parts-directory}/etc/profile.ini
+
+[eggbasket]
+recipe = z3c.recipe.eggbasket
+eggs = grok
+url = http://grok.zope.org/releaseinfo/grok-eggs-1.1.tgz
+
+[waeupdocs]
+recipe = collective.recipe.sphinxbuilder
+eggs = waeup.kofa[docs]
+source = ${buildout:directory}/docs/source
+build = ${buildout:directory}/docs/build
+
+# Collect test coverage data.
+[coverage-detect]
+recipe = zc.recipe.testrunner
+eggs = waeup.kofa[beaker]
+defaults = ['--tests-pattern', '^f?tests$', '-v', '--coverage', 'coverage']
+
+# Create a coverage report.
+# Make sure to run bin/coverage-detect to collect the data for the report
+# first!
+[coverage-report]
+recipe = zc.recipe.egg
+eggs = z3c.coverage
+scripts = coveragereport
+arguments = ('${buildout:parts-directory}/coverage-detect/coverage', '${buildout:directory}/coverage-report')
+
+[lxml]
+recipe = z3c.recipe.staticlxml
+egg = lxml
+
+[diazo]
+recipe = zc.recipe.egg
+eggs =
+    diazo [wsgi]
+    PasteScript
+    waeup.kofa [beaker,diazo]
Index: main/waeup.kofa/branches/uli-py3/docs/INSTALL.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/INSTALL.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/INSTALL.txt	(revision 16045)
@@ -0,0 +1,555 @@
+Installation of Kofa
+####################
+
+These are generic installation instructions for the WAeUP_ Kofa
+student information portal and customized versions thereof.
+
+For specific aspects of this package please refer to the local README
+file.
+
+Please note, that **only Linux-based installs** are described and actively
+supported. We recommend use of Debian_ / Ubuntu_.
+
+.. note:: This means we do not actively support Windows(tm)!
+
+.. contents:: Table of Contents
+   :local:
+
+.. _prerequisites:
+
+
+Quick Install on Ubuntu 18.04
+*****************************
+
+If you want to install Kofa on a running Ubuntu system, the following quick
+steps might be sufficient::
+
+  ## install neccessary system packages as superuser
+  $ sudo apt-get update && apt-get install -y  # update system
+  $ sudo apt-get install python2.7-dev libxml2-dev libxslt1-dev zlib1g-dev python-virtualenv libssl-dev libffi-dev libjpeg-dev libfreetype6-dev libtiff-dev libopenjp2-7-dev
+
+  ## get sources as regular user
+  $ wget https://pypi.python.org/packages/ed/0f/0b30fcf8c113b11967c1481668b38b4b9e32f256eb366f73b70ff960872f/waeup.kofa-1.5.tar.gz && tar -xzf waeup.kofa-1.5.tar.gz
+  $ cd waeup.kofa-1.5
+
+  ## create python sandbox and build kofa
+  $ virtualenv py27
+  $ source py27/bin/activate
+  $ pip install --upgrade pip
+  $ python bootstrap.py
+  $ ./bin/buildout
+
+To run Kofa, now do::
+
+  ## startup Kofa in foreground (recommended for first start)
+  $ ./bin/kofactl fg
+  ## press CTRL-D to stop the kofa server
+
+  ## startup Kofa in background
+  $ ./bin/kofactl start
+  $ ./bin/kofactl status  # check status, running or not
+  $ ./bin/kofactl stop    # stop running instance (also 'restart' possible)
+
+When Kofa is running, you can access the portal on port 8080. Open
+
+  http://localhost:8080/
+
+and enter the credentials (username 'grok', password 'grok' by
+default) and name and add a `University`.
+
+Congratulations, you got Kofa running.
+
+
+Prerequisites
+*************
+
+The Kofa packages are based on `Grok`_, which is a Python_
+framework for agile web application development. Grok_ itself is based
+on `Zope`_.
+
+Both, Grok_ and Zope_, are written in the `Python`_ programming
+language (with parts written in C). You therefore have to have
+`Python`_ installed (including `Python`_ headers) in version 2.7
+(Python 3.x is currently not supported).
+
+
+Preparing the System
+====================
+
+For a Kofa install we need of course `Python`_ installed as well as
+some standard developer tools like C compilers, C libraries, etc.
+
+What you need (Debian/Ubuntu package names in brackets):
+
+* Python 2.7 (``python2.7``)
+
+* Python 2.7 development files (``python2.7-dev``)
+
+* A C-Compiler with basic C developer libraries (``build-essential``)
+
+* A subversion client (``subversion``)
+
+* XML and XSLT development libraries (``libxml2-dev``, ``libxslt1-dev``)
+
+* Other helper libraries, especially for image processing support
+  (``zlib1g-dev``, ``libssl-dev``, ``libffi-dev``, ``libjpeg-dev``,
+  ``libfreetype6-dev``, ``libtiff-dev`` ``libopenjpeg-dev``)
+
+* enscript (``enscript``) [optional]
+
+  This is only needed if you want test coverage reports.
+
+All these packages can be installed on Debian systems like this::
+
+  $ sudo apt-get install python2.7 python2.7-dev subversion \
+                         build-essential enscript libxml2-dev libxslt1-dev \
+                         zlib1g-dev libssl-dev libffi-dev libjpeg-dev \
+                         libfreetype6-dev libtiff-dev libopenjpeg-dev
+
+Afterwards you should be able to enter::
+
+  $ python2.7
+
+at the commandline and get a Python_ prompt. Quit the interpreter
+pressing <CTRL-D>.
+
+The above packages have been tested to be sufficient for running Kofa
+on Ubuntu. We check this with Docker images created at
+
+   https://github.com/WAeUP/kofa-docker
+
+This project provides tags that refer to certain Kofa versions.
+
+Please note: to make freetype2 work correctly with Kofa [#]_, you have to
+fix a link::
+
+  (root) $  ln -s /usr/include/freetype2 /usr/include/freetype2/freetype
+
+
+
+
+Installing `virtualenv`
+=======================
+
+We strongly suggest use of `virtualenv`_ to create Python_ sandboxes
+where you can run your code without touching any other installations.
+
+`virtualenv`_ is also the only possibility to build a Kofa install
+without the need of superuser permissions. In short: `virtualenv`_
+will make your life easier. Use it.
+
+Detailed install instructions for `virtualenv`_ can be found on
+http://www.virtualenv.org/en/latest/virtualenv.html#installation.
+
+The short way for a user install (no superuser perms required) is like
+this::
+
+  $ curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.11.4.tar.gz#md5=9accc2d3f0ec1da479ce2c3d1fdff06e
+  $ tar xvfz virtualenv-1.11.4.tar.gz
+  $ cd virtualenv-1.11.4
+  $ python virtualenv.py py27
+
+Superusers can install `virtualenv`_ system-wide much easier. On
+`Debian`_/`Ubuntu`_ systems where you have superuser permissions, you
+can install `virtualenv`_ via::
+
+  $ sudo apt-get install python-virtualenv
+
+*Or*, they install if `pip` first::
+
+  $ sudo apt-get install python-pip
+
+and then use `pip` to install `virtualenv`_::
+
+  $ sudo pip install virtualenv
+
+
+Creating a Sandbox
+==================
+
+After installing `virtualenv`_ you can install local sandboxes like
+this (if `virtualenv` is installed system-wide)::
+
+  $ virtualenv --no-site-packages py27
+
+*or* like this::
+
+  $ python /path/to/my/virtualenv.py --no-site-packages py27
+
+where ``py27`` is a directory in the filesystem where your sandbox
+will be created. `virtualenv` will also create this directory for
+you. While you can pick any sandbox name you want, it is recommended
+to use a sandbox name that is short and at the same time reflects the
+Python version used (here: Python 2.7).
+
+By passing the ``no-site-packages`` switch we tell `virtualenv` to
+provide a clean environment without any extra-packages installed
+systemwide. More recent versions of `virtualenv`_ have this option set
+by default.
+
+Another often used option of `virtualenv`_ is ``-p``. With ``-p`` you
+can tell `virtualenv`_ which Python executable to use as base for the
+new sandbox.
+
+You now can activate the sandbox by doing::
+
+  $ source py27/bin/activate
+  (py27)$
+
+You will notice that the input prompt changes, indicating the name of
+the sandbox activated.
+
+To deactivate the sandbox at any time, enter::
+
+  (py27)$ deactivate
+
+and the prompt will be the same as before the activation.
+
+For the following steps make sure the sandbox is active.
+
+
+Getting the Sources
+===================
+
+Now, as the sandbox is activated (see above, if not), we can fetch the
+sources for Kofa.
+
+Source from PyPI
+----------------
+
+Official releases are always uploaded to PyPI. Please have a look at
+
+  http://pypi.python.org/pypi/waeup.kofa
+
+where you can download all you need.
+
+From the commandline you can also get sources like this::
+
+  (py27)$ wget https://pypi.python.org/packages/source/w/waeup.kofa/waeup.kofa-1.5.tar.gz
+
+where ``1.5`` is, of course, only one possible version.
+
+
+Source from Subversion
+----------------------
+
+If you have access to the (restricted) developer repository (if you
+don't know, then you have not), then you can use the Subversion_
+client `svn` and checkout the main devel branch of the `waeup.kofa`
+package like this::
+
+  (py27)$ svn co https://svn.waeup.org/repos/main/waeup.kofa/trunk
+
+The command should fetch the Kofa base package sources for you and
+will put everything in a new directory ``trunk/``.
+
+Now enter the new directory::
+
+  (py27)$ cd trunk/
+
+and you can start building the real package.
+
+.. note:: If you not want to work with the `waeup.kofa` package but a
+          customized package (like ``waeup.aaue`` or similar), then
+          you can use the above checkout command but with
+          ``waeup.kofa`` replaced by the name of the customized
+          package::
+
+            (py27)$ svn co https://svn.waeup.org/repos/main/<PKG-NAME>/trunk
+
+          For the exact link, please refer to the README.txt file in
+          your package.
+
+
+Bootstrapping the Build
+=======================
+
+After `Creating a Sandbox`_ and `Getting the Sources`_ we have to
+initialize the newly created development environment::
+
+  (py27)$ python bootstrap.py
+
+This should create a local ``bin/`` directory with at least a script
+``buildout`` in it.
+
+If bootstrapping fails, chances are, that you need the `distribute`
+package installed. In a virtual environment you can install it without
+interfering with other packages installed on your server::
+
+  (py27)$ pip install --upgrade distribute
+
+Afterwards you have to retry the bootstrapping, until you get a
+working ``buildout`` script in the local ``bin/`` directory.
+
+Once the ``buildout`` script exists, you do not have to activate the
+virtual envronment any more (although you can).
+
+
+Part 1: Single Client Setup
+***************************
+
+You can run Kofa with a single client (useful for evaluation,
+development, etc.) or with multiple clients running in parallel. The
+latter setup is useful in productive environments with many thousands
+users and called a ``ZEO`` install. We will cover both setup
+types. The first is covered here, the latter one is covered in `Part
+2: ZEO Install`_.
+
+
+Building the Package
+====================
+
+In the sources directory (``trunk/``) you have to prepare the
+project to fetch needed components (eggs), compile C-code parts,
+etc. This is done by a single command ``buildout``::
+
+  $ ./bin/buildout
+
+If this is your first install of some Grok-related project, this step
+will need some time as lots of sources have to be fetched, many
+components must be compiled, etc.
+
+This step must be redone whenever you change something in
+``buildout.cfg``, ``buildout-zeo.cfg``, or ``setup.py``.
+
+Note that if you have more than one sandbox for a Zope-based web
+application, it will probably make sense to share the eggs between the
+different sandboxes.  You can tell ``zc.buildout`` to use a central
+eggs directory by creating ``~/.buildout/default.cfg`` with the
+following contents::
+
+  [buildout]
+  eggs-directory = /home/bruno/buildout-eggs
+
+where the given directory should be exist beforehand. All eggs of all
+`buildout` projects will then be stored in this directory (and not
+fetched anew if already existing there).
+
+
+Start the Instance
+==================
+
+You should be able now to start the created instance by doing::
+
+  $ ./bin/kofactl fg
+
+Alternatively you can do::
+
+  $ bin/paster serve parts/etc/deploy-themed.ini
+
+The port numbers where Kofa is running on your server are defined in
+`buildout.cfg` under ``[kofa_params]``.
+
+If you now point a browser to the right port on your server, for example::
+
+  localhost:8080
+
+you should get a login pop-up, where you can login as superuser with
+``grok`` and ``grok`` as username/password (Kofa base package only!).
+
+You can stop the instance by pressing <CTRL-C>.
+
+If you are connected and logged in,
+you should be able to add the grok-based applications
+(such as ``University``) from the menu.
+
+Add an instance of ``University`` and click on the link next to the
+then visible entry in the list of installed applications.
+
+Running the Tests
+=================
+
+All WAeUP_ packages come with comprehensive test suites ensuring the
+software quality also after changes and updates.
+
+The package tests are easily run by executing the test runner
+from the ``bin`` directory::
+
+  $ ./bin/test
+
+Use the ``-c`` option to get coloured output. Use the ``--pdb`` option
+to start the Python_ debugger when a test fails.
+
+
+Part 2: ZEO Install
+*******************
+
+Each ZEO install consists of at least one ZEO server and normally two
+or more ZEO clients. While the ZEO server is meant to manage the ZODB
+database for clients, the clients connect to the outside world, listen
+for request and do the real dataprocessing.
+
+We prepared a `buildout` configuration that sets up one server
+configuration and two client configs. This configuration is in
+``buildout-zeo.cfg``.
+
+Generating the ZEO Setup
+========================
+
+To install Kofa ZEO-based you can run `buildout` with the given
+(or your own) configuration file like this::
+
+  $ ./bin/buildout -c buildout-zeo.cfg
+
+This should generate all scripts necessary to run servers, clients,
+etc.
+
+Starting ZEO Servers and Clients
+================================
+
+First start the server::
+
+  $ ./bin/zeo_server start
+
+Clients can be started by doing::
+
+  $ ./bin/zeo_client1 start
+  $ ./bin/zeo_client2 start
+
+This will start both clients in daemon mode.
+
+Instead of ``start`` you can, as usually, start an instance in
+foreground (``fg``), etc. You know the drill.
+
+
+Manually Starting ZEO Clients
+=============================
+
+This is normally not neccessary.
+
+``zeo_clientN`` scripts are basically wrappers around calls to
+``bin/paster``. You can bypass this wrapper and start a client
+'manually' like this::
+
+  $ ./bin/paster serve --pid-file var/zeo1.pid --daemon \
+        pars/etc/zeo1.ini
+
+It is important to give a pid-file as `paster` otherwise can not start
+different clients (they would all refer to the same pid file
+`paster.pid` and refuse to start after the first client was started).
+
+
+Setup (parameters, ports, etc.)
+===============================
+
+By default the server will listen on port 8100 for requests from
+localhost (not: from the outside world).
+
+You can change ZEO server settings in the ``[zeo_server]`` section
+of ``buildout-zeo.conf``. Run `buildout` afterwards.
+
+The clients will listen on port 8081 and 8082. You can change settings
+in ``etc/zeo1.ini.in`` and ``etc/zeo2.ini.in`` respectively. Run
+buildout after any change.
+
+If you want to change the paster wrapper for any zeo client, you can
+edit ``etc/zeo1.conf`` and/or ``etc/zeo2.conf``. Run buildout
+afterwards.
+
+
+Stopping ZEO Servers and Clients
+================================
+
+Given everything was started in daemon mode, first stop the clients::
+
+  $ ./bin/zeo_client1 stop
+  $ ./bin/zeo_client2 stop
+
+Then stop the ZEO server::
+
+  $ ./bin/zeo_server stop
+
+
+Creating New ZEO Clients
+========================
+
+You want more ZEO clients to be created by `buildout`? Easy. Three
+steps are neccessary.
+
+1. Create config files in `etc/`
+--------------------------------
+
+Each client needs two configuration files:
+
+  - ``etc/zeoN.conf``
+       configuring the paster wrapper
+
+  - ``etc/zeoN.ini``
+       configuring the runtime config, ports, etc.
+
+Just copy over these files from the already existing zeo1/zeo2 files
+and replace ``zeo1`` or ``zeo2`` with your new name.
+
+2. Update buildout-zeo.cfg
+--------------------------
+
+Here, inside ``buildourt-zeo.cfg`` also three steps are needed.
+
+* 2.1. Create new .ini and .conf entries
+
+  The .conf and .ini files in etc/ are only templates that have to be
+  generated in their really used final location. In buildout-zeo.cfg
+  you can care for this by creating a new ``[zeoN_ini]`` and
+  ``[zeoN_conf]`` option (replacing ``N`` with a number, of course).
+
+  Just copy over existing entries and replace the mentions of ``zeo1``
+  or ``zeo2`` by your ``zeoN``.
+
+* 2.2. Create a new ``zeo_clientN`` entry
+
+  Then you have to create an entry that will generate the
+  ``zeo_clientN`` script. Again, just copy over an existing
+  ``[zeo_client1]`` entry and replace ``1`` withg your client number.
+
+* 2.3. Register the new sections in ``[buildout]`` section
+
+  When done with the above: add the new section in ``[buildout]``::
+
+    [buildout]
+      ...
+      <old entries...>
+      ...
+      zope_conf_zeo_5
+      zeo5_ini
+      zeo_client5
+
+  depending on how you named your new sections.
+
+3. Rerun ``buildout``
+---------------------
+
+When adding or removing client/server instances, make sure to stop all
+running servers/clients before rerunning buildout.
+
+To activate the new setup, rerun buildout::
+
+  $ bin/buildout -c buildout-zeo.cfg
+
+This should generate any new clients and remove older ones or just
+update configuration files.
+
+
+Considerations
+==============
+
+There are some things in the current buildout-zeo.cfg we might do not
+want. It extends the regular ``buildout.cfg`` so that we do not have
+to repeat most sections but the ``parts`` in ``[buildout]`` have to be
+listed.
+
+We need, however, not everything with a ZEO-deploy that is listed in a
+default buildout. We might do not need docs, no profiling, etc. Also a
+regular non-ZEO kofactl might not make to much sense. Therefore all
+this might be subject to changes.
+
+.. _Debian: http://www.debian.org/
+.. _Grok: http://grok.zope.org/
+.. _Python: http://www.python.org/
+.. _Subversion: http://subversion.apache.org/
+.. _Ubuntu: http://www.ubuntu.com/
+.. _virtualenv: http://www.virtualenv.org/en/latest/
+.. _WAeUP: https://www.waeup.org/
+.. _Zope: http://www.zope.org/
+.. _zc.buildout: http://cheeseshop.python.org/pypi/zc.buildout
+.. [#] Strictly speaking, this link fix is required by ``Pillow``, a
+   library used by Kofa to provide higher-level image processing.
Index: main/waeup.kofa/branches/uli-py3/docs/Makefile
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/Makefile	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/Makefile	(revision 16045)
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  dirhtml   to make HTML files named index.html in directories"
+	@echo "  pickle    to make pickle files"
+	@echo "  json      to make JSON files"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  qthelp    to make HTML files and a qthelp project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WAeUPStudentInformationandRegistrationPortalKOFA.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WAeUPStudentInformationandRegistrationPortalKOFA.qhc"
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
Index: main/waeup.kofa/branches/uli-py3/docs/make.bat
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/make.bat	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/make.bat	(revision 16045)
@@ -0,0 +1,113 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+set SPHINXBUILD=sphinx-build
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html      to make standalone HTML files
+	echo.  dirhtml   to make HTML files named index.html in directories
+	echo.  pickle    to make pickle files
+	echo.  json      to make JSON files
+	echo.  htmlhelp  to make HTML files and a HTML help project
+	echo.  qthelp    to make HTML files and a qthelp project
+	echo.  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  changes   to make an overview over all changed/added/deprecated items
+	echo.  linkcheck to check all external links for integrity
+	echo.  doctest   to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WAeUPStudentInformationandRegistrationPortalKOFA.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WAeUPStudentInformationandRegistrationPortalKOFA.ghc
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end
Index: main/waeup.kofa/branches/uli-py3/docs/source/_static/default.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/_static/default.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/_static/default.css	(revision 16045)
@@ -0,0 +1,257 @@
+/*
+ * default.css
+ * ~~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- default theme.
+ *
+ * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ * Customized by Henrik Bettermann
+ */
+
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+    font-family: sans-serif;
+    font-size: 100%;
+    background-color: #11303d;
+    color: #000;
+    margin: 0;
+    padding: 0;
+}
+
+div.document {
+    background-color: #1c4e63;
+}
+
+div.documentwrapper {
+    float: left;
+    width: 100%;
+}
+
+div.bodywrapper {
+    margin: 0 0 0 230px;
+}
+
+div.body {
+    background-color: #ffffff;
+    color: #000000;
+    padding: 0 20px 30px 20px;
+}
+
+div.footer {
+    color: #ffffff;
+    width: 100%;
+    padding: 9px 0 9px 0;
+    text-align: center;
+    font-size: 75%;
+}
+
+div.footer a {
+    color: #ffffff;
+    text-decoration: underline;
+}
+
+div.related {
+    background-color: #133f52;
+    line-height: 30px;
+    color: #ffffff;
+}
+
+div.related a {
+    color: #ffffff;
+}
+
+div.sphinxsidebar {
+}
+
+div.sphinxsidebar h3 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: #ffffff;
+    font-size: 1.4em;
+    font-weight: normal;
+    margin: 0;
+    padding: 0;
+}
+
+div.sphinxsidebar h3 a {
+    color: #ffffff;
+}
+
+div.sphinxsidebar h4 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: #ffffff;
+    font-size: 1.3em;
+    font-weight: normal;
+    margin: 5px 0 0 0;
+    padding: 0;
+}
+
+div.sphinxsidebar p {
+    color: #ffffff;
+}
+
+div.sphinxsidebar p.topless {
+    margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+    margin: 10px;
+    padding: 0;
+    color: #ffffff;
+}
+
+div.sphinxsidebar a {
+    color: #98dbcc;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+
+
+/* -- hyperlink styles ------------------------------------------------------ */
+
+a {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:visited {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+
+
+/* -- body styles ----------------------------------------------------------- */
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+    font-family: 'Trebuchet MS', sans-serif;
+    background-color: #f2f2f2;
+    font-weight: normal;
+    color: #20435c;
+    border-bottom: 1px solid #ccc;
+    margin: 20px -20px 10px -20px;
+    padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 0; font-size: 200%; }
+div.body h2 { font-size: 160%; }
+div.body h3 { font-size: 140%; }
+div.body h4 { font-size: 120%; }
+div.body h5 { font-size: 110%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+    color: #c60f0f;
+    font-size: 0.8em;
+    padding: 0 4px 0 4px;
+    text-decoration: none;
+}
+
+a.headerlink:hover {
+    background-color: #c60f0f;
+    color: white;
+}
+
+div.body p, div.body dd, div.body li {
+    text-align: left;
+    line-height: 130%;
+}
+
+div.admonition p.admonition-title + p {
+    display: inline;
+}
+
+div.admonition p {
+    margin-bottom: 5px;
+}
+
+div.admonition pre {
+    margin-bottom: 5px;
+}
+
+div.admonition ul, div.admonition ol {
+    margin-bottom: 5px;
+}
+
+div.note {
+    background-color: #eee;
+    border: 1px solid #ccc;
+}
+
+div.seealso {
+    background-color: #ffc;
+    border: 1px solid #ff6;
+}
+
+div.topic {
+    background-color: #eee;
+}
+
+div.warning {
+    background-color: #ffe4e4;
+    border: 1px solid #f66;
+}
+
+p.admonition-title {
+    display: inline;
+}
+
+p.admonition-title:after {
+    content: ":";
+}
+
+pre {
+    padding: 5px;
+    background-color: #eeffcc;
+    color: #333333;
+    line-height: 120%;
+    border: 1px solid #ac9;
+    border-left: none;
+    border-right: none;
+}
+
+tt {
+    background-color: #ecf0f3;
+    padding: 0 1px 0 1px;
+    font-size: 0.95em;
+}
+
+th {
+    background-color: #ede;
+}
+
+.warning tt {
+    background: #efc2c2;
+}
+
+.note tt {
+    background: #d6d6d6;
+}
+
+.viewcode-back {
+    font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+    background-color: #f4debf;
+    border-top: 1px solid #ac9;
+    border-bottom: 1px solid #ac9;
+}
Index: main/waeup.kofa/branches/uli-py3/docs/source/_templates/mainpage.template
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/_templates/mainpage.template	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/_templates/mainpage.template	(revision 16045)
@@ -0,0 +1,102 @@
+{% extends "layout.html" %}
+{% set title = 'Overview' %}
+{% block body %}
+
+<h1>{{ docstitle }}</h1>
+<p>
+  This is official documentation for waeup.kofa {{ release }}.
+  {% if last_updated %}Last modifed on {{ last_updated }}.{% endif %}
+</p>
+
+<table class="contentstable" align="center">
+  <tr>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("intro") }}">Introduction</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("contents") }}">Table of Contents</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+  </tr>
+
+  <tr>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+             href="{{ pathto("userdocs/index") }}">User Handbook</a><br />
+        <span class="linkdescr">
+          Documentation for officers and admins
+        </span>
+      </p>
+    </td>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("developerdocs/index") }}">Developer Handbook</a><br />
+        <span class="linkdescr">
+          <a href="{{ pathto("developerdocs/api/index") }}">API</a> and other pieces
+          relevant for developers
+        </span>
+      </p>
+    </td>
+  </tr>
+
+  <tr>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("changes") }}">What's new?</a><br />
+        <span class="linkdescr">Changes made in each release</span>
+      </p>
+    </td>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("genindex") }}">Index</a><br />
+        <span class="linkdescr">All Python functions and classes</span>
+      </p>
+    </td>
+  </tr>
+  <tr>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("bugs") }}">Reporting Bugs</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("license") }}">License</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+  </tr>
+  <tr>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("documentation") }}">About the Documentation</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+    <td width="50%">
+      <p class="biglink">
+        <a class="biglink"
+           href="{{ pathto("copyright") }}">Copyright</a><br />
+        <span class="linkdescr"></span>
+      </p>
+    </td>
+  </tr>
+</table>
+
+
+{% endblock %}
Index: main/waeup.kofa/branches/uli-py3/docs/source/bugs.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/bugs.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/bugs.rst	(revision 16045)
@@ -0,0 +1,18 @@
+.. _reporting-bugs:
+
+Reporting Bugs in `waeup.kofa`
+******************************
+
+The developers would like to know of any deficiencies you find in
+`waeup.kofa`.
+
+Also telling about errors and shortcomings in the documentation are
+welcome. Please write to ``henrik at waeup dot org`` if you find any.
+
+.. seealso::
+
+   `How to Report Bugs Effectively
+   <http://www.chiark.greenend.org.uk/~sgtatham/bugs.html>`_ Article
+   which goes into some detail about how to create a useful bug
+   report. This describes what kind of information is useful and why
+   it is useful.
Index: main/waeup.kofa/branches/uli-py3/docs/source/changes.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/changes.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/changes.rst	(revision 16045)
@@ -0,0 +1,1 @@
+.. include:: ../../CHANGES.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/conf.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/conf.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/conf.py	(revision 16045)
@@ -0,0 +1,239 @@
+# -*- coding: utf-8 -*-
+#
+# WAeUP Student Information and Registration Portal (Kofa) documentation build configuration file, created by
+# sphinx-quickstart on Thu Jul 29 10:41:59 2010.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+import pkg_resources
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.todo',
+    'sphinx.ext.coverage',
+    'sphinx.ext.ifconfig',
+    'sphinx.ext.viewcode',
+    'repoze.sphinx.autointerface'
+    ]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'WAeUP.Kofa'
+copyright = u'2011, WAeUP Team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = pkg_resources.get_distribution('waeup.kofa').version
+#version = '0.2dev'
+# The full version, including alpha/beta/rc tags.
+#release = '0.2dev'
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+#html_theme = 'sphinx_rtd_theme'
+html_theme = 'sphinxdoc'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+html_title = "%s %s Documentation" % (project, release)
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+html_favicon = "_static/favicon.ico"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+#html_additional_pages = {
+#    'index': os.path.join(os.path.dirname(__file__), 'docindex.template')}
+html_additional_pages = { 'index': 'mainpage.template'}
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'WAeUPStudentInformationandRegistrationPortalKofadoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'WAeUPKofaDocumentation.tex', u'WAeUP.Kofa Documentation',
+   u'WAeUP Team', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
+
+
+def autodoc_skip_dotnamed_members(app, what, name, obj, skip, optios):
+    """In autodoc skip members with dot in name.
+
+    Sphinx chokes when it comes to objects that have dots in their name.
+
+    We will make this skipping an option of the then used
+    `waeup.sphinx.autodoc` package.
+
+    This is clearly a workaround and not a solution. This is a
+    workaround, not a solution.
+
+    In the long run our patch to Sphinx core will hopefully make it into
+    the Sphinx package itself. This would be a solution, not a
+    workaround.
+    """
+    if "." in name:
+        ##  Uncomment the following line to see what is skipped.
+        # print("SKIPPING %r %r %r" % (what, name, obj))
+        return True
+    return False
+
+def setup(app):
+    app.connect('autodoc-skip-member', autodoc_skip_dotnamed_members)
Index: main/waeup.kofa/branches/uli-py3/docs/source/contents.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/contents.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/contents.rst	(revision 16045)
@@ -0,0 +1,25 @@
+.. _contents:
+
+Contents
+========
+
+.. toctree::
+   :maxdepth: 2
+
+   intro
+   userdocs/index
+   developerdocs/index
+   changes
+   license
+   copyright
+   glossary
+   bugs
+   documentation
+
+Indices and Tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+* :ref:`glossary`
Index: main/waeup.kofa/branches/uli-py3/docs/source/copyright.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/copyright.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/copyright.rst	(revision 16045)
@@ -0,0 +1,10 @@
+Copyright
+*********
+
+`waeup.kofa` and this documentation is:
+
+Copyright © 2009-2010 The WAeUP Group. All rights reserved.
+
+----------
+
+See :ref:`license` for complete license and permissions information.
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_accesscode.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`accesscodes.accesscode` -- AC Components
+*****************************************
+
+.. automodule:: waeup.kofa.accesscodes.accesscode
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 16045)
@@ -0,0 +1,12 @@
+`accesscodes.browser` -- AC UI Components
+*****************************************
+
+Probably due to a bug in Sphinx autodoc couldn't import/find some classes and
+reported error: "grokcore, please check your spelling and sys.path".
+Thus, automodule has been temporarily disabled.
+
+
+..
+  .. automodule:: waeup.kofa.accesscodes.browser
+     :members:
+     :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`accesscodes.catalog` -- AC Catalog Components
+**********************************************
+
+.. automodule:: waeup.kofa.accesscodes.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`accesscodes.interfaces` -- AC Interfaces
+*****************************************
+
+.. automodule:: waeup.kofa.accesscodes.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`accesscodes.workflow` - AC Workflow Components
+***********************************************
+
+.. automodule:: waeup.kofa.accesscodes.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_accesscodes.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_accesscodes.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_accesscodes.rst	(revision 16045)
@@ -0,0 +1,24 @@
+`accesscodes` -- Access Codes Package
+*************************************
+
+(see also: :ref:`api-accesscodes-modules`)
+
+.. automodule:: waeup.kofa.accesscodes
+   :members:
+   :undoc-members:
+
+.. _api-accesscodes-modules:
+
+Submodules
+===========
+
+This package contains the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   accesscodes/api_accesscode
+   accesscodes/api_browser
+   accesscodes/api_catalog
+   accesscodes/api_interfaces
+   accesscodes/api_workflow
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_app.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_app.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_app.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`app` -- University Root (Site) Module
+**************************************
+
+.. automodule:: waeup.kofa.app
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_applicants.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_applicants.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_applicants.rst	(revision 16045)
@@ -0,0 +1,29 @@
+`applicants` - Application Components Package
+*********************************************
+
+(see also: :ref:`api-applicants-modules`)
+
+.. automodule:: waeup.kofa.applicants
+   :members:
+   :undoc-members:
+
+.. _api-applicants-modules:
+
+Submodules
+----------
+
+This package bundles the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   applicants/api_interfaces
+   applicants/api_root
+   applicants/api_container
+   applicants/api_applicant
+   applicants/api_authentication
+   applicants/api_batching
+   applicants/api_browser
+   applicants/api_permissions
+   applicants/api_workflow
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_async.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_async.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_async.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`async` -- Asynchronous Job Handling Module
+*******************************************
+
+.. automodule:: waeup.kofa.async
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_authentication.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_authentication.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_authentication.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`authentication` -- Authentication Components Module
+****************************************************
+
+.. automodule:: waeup.kofa.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_beaker.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_beaker.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_beaker.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`beaker` -- dolmen.beaker Configuration Module
+**********************************************
+
+.. automodule:: waeup.kofa.beaker
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_browser.rst	(revision 16045)
@@ -0,0 +1,31 @@
+`browser` -- Viewing Components Package
+***************************************
+
+(see also: :ref:`api-browser-modules`)
+
+.. automodule:: waeup.kofa.browser
+   :members:
+   :undoc-members:
+
+.. _api-browser-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   browser/api_async.rst
+   browser/api_breadcrumbs.rst
+   browser/api_captcha.rst
+   browser/api_exceptions.rst
+   browser/api_fileviewlets.rst
+   browser/api_interfaces.rst
+   browser/api_layout.rst
+   browser/api_pages.rst
+   browser/api_pdf.rst
+   browser/api_reports.rst
+   browser/api_viewlets.rst
+   
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_catalog.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`catalog` -- Cataloging Components Module
+*****************************************
+
+.. automodule:: waeup.kofa.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_configuration.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_configuration.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_configuration.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`configuration` -- Portal Configuration Components Module
+**********************************************************
+
+.. automodule:: waeup.kofa.configuration
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_datacenter.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_datacenter.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_datacenter.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`datacenter` -- Datacenter Module
+*********************************
+
+.. automodule:: waeup.kofa.datacenter
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_hostels.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_hostels.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_hostels.rst	(revision 16045)
@@ -0,0 +1,26 @@
+`hostels` -- Accommodation Section Components Package
+*****************************************************
+
+(see also: :ref:`api-accommodation-modules`)
+
+.. automodule:: waeup.kofa.hostels
+   :members:
+   :undoc-members:
+
+.. _api-accommodation-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   hostels/api_hostel
+   hostels/api_container
+   hostels/api_browser
+   hostels/api_catalog
+   hostels/api_permissions
+   hostels/api_vocabularies
+   hostels/api_interfaces
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_image.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_image.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_image.rst	(revision 16045)
@@ -0,0 +1,22 @@
+`image` -- Image File Components Package
+****************************************
+
+(see also: :ref:`api-image-modules`)
+
+.. automodule:: waeup.kofa.image
+   :members:
+   :undoc-members:
+
+.. _api-image-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   image/api_image
+   image/api_schema
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_imagestorage.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_imagestorage.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_imagestorage.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`imagestorage` -- Image and File Storage Components Module
+**********************************************************
+
+.. automodule:: waeup.kofa.imagestorage
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,10 @@
+`interfaces` - Main Interfaces Module
+*************************************
+
+.. automodule:: waeup.kofa.interfaces
+   :members:
+   :undoc-members:
+
+.. autointerface:: waeup.kofa.interfaces.IUniversity
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_language.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_language.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_language.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`language` -- User Language Components Module
+*********************************************
+
+.. automodule:: waeup.kofa.language
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_maintenance.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_maintenance.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_maintenance.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`maintenance` -- Maintenance Helpers
+************************************
+
+.. automodule:: waeup.kofa.maintenance
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_mandates.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_mandates.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_mandates.rst	(revision 16045)
@@ -0,0 +1,23 @@
+`mandates` -- Mandates Components Package
+*****************************************
+
+(see also: :ref:`api-mandates-modules`)
+
+.. automodule:: waeup.kofa.mandates
+   :members:
+   :undoc-members:
+
+.. _api-mandates-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   mandates/api_browser
+   mandates/api_container
+   mandates/api_mandate
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_meta.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_meta.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_meta.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`meta` -- Kofa Components Grokkers Module
+*****************************************
+
+.. automodule:: waeup.kofa.meta
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_objecthistory.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_objecthistory.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_objecthistory.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`objecthistory` -- Components for Object Histories Module
+*********************************************************
+
+.. automodule:: waeup.kofa.objecthistory
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_payments.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_payments.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_payments.rst	(revision 16045)
@@ -0,0 +1,22 @@
+`payments` -- Payment Components Package
+****************************************
+
+(see also: :ref:`api-payments-modules`)
+
+.. automodule:: waeup.kofa.payments
+   :members:
+   :undoc-members:
+
+.. _api-payments-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   payments/api_payment
+   payments/api_container
+   payments/api_interfaces
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_permissions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_permissions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_permissions.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`permissions` -- Permissions and Roles Components Module
+********************************************************
+
+.. automodule:: waeup.kofa.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_reports.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_reports.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_reports.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`reports` -- Report Generation Components Module
+************************************************
+
+.. automodule:: waeup.kofa.reports
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schema.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schema.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schema.rst	(revision 16045)
@@ -0,0 +1,21 @@
+`schema` -- Special Schema Fields Package
+*****************************************
+
+(see also: :ref:`api-schema-modules`)
+
+.. automodule:: waeup.kofa.schema
+   :members:
+   :undoc-members:
+
+.. _api-schema-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   schema/api_field
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schoolgrades.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schoolgrades.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_schoolgrades.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`schoolgrades` -- School Grades Components Module
+*************************************************
+
+.. automodule:: waeup.kofa.schoolgrades
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_smtp.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_smtp.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_smtp.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`smtp` -- SMTP Services Components Module
+*****************************************
+
+.. automodule:: waeup.kofa.smtp
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_sourcefactory.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_sourcefactory.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_sourcefactory.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`sourcefactory` -- Contextual Sources Module
+********************************************
+
+.. automodule:: waeup.kofa.sourcefactory
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_startup.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_startup.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_startup.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`startup` -- WSGI Application Factories Module
+**********************************************
+
+.. automodule:: waeup.kofa.startup
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_students.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_students.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_students.rst	(revision 16045)
@@ -0,0 +1,36 @@
+`students` -- Students Section Components Package
+*************************************************
+
+(see also: :ref:`api-students-modules`)
+
+.. automodule:: waeup.kofa.students
+   :members:
+   :undoc-members:
+
+.. _api-students-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   students/api_student
+   students/api_studycourse
+   students/api_studylevel
+   students/api_authentication
+   students/api_permissions
+   students/api_batching
+   students/api_browser
+   students/api_viewlets
+   students/api_catalog
+   students/api_container
+   students/api_dynamicroles
+   students/api_payments
+   students/api_accommodation
+   students/api_workflow
+   students/api_vocabularies
+   students/api_utils
+   students/api_interfaces
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_university.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_university.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_university.rst	(revision 16045)
@@ -0,0 +1,30 @@
+`university` -- University Components Package
+*********************************************
+
+(see also: :ref:`api-university-modules`)
+
+.. automodule:: waeup.kofa.university
+   :members:
+   :undoc-members:
+
+.. _api-university-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   university/api_interfaces
+   university/api_catalog
+   university/api_vocabularies
+   university/api_batching
+   university/api_facultiescontainer
+   university/api_faculty
+   university/api_department
+   university/api_certificatescontainer
+   university/api_coursescontainer
+   university/api_course
+   university/api_certificate
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_userscontainer.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_userscontainer.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_userscontainer.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`userscontainer` -- Users (Principals) Module
+*********************************************
+
+.. autoclass:: waeup.kofa.userscontainer.UsersContainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_utils.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_utils.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_utils.rst	(revision 16045)
@@ -0,0 +1,24 @@
+`utils` -- Utilities Package
+****************************
+
+(see also: :ref:`api-utils-modules`)
+
+.. automodule:: waeup.kofa.utils
+   :members:
+   :undoc-members:
+
+.. _api-utils-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   utils/api_batching
+   utils/api_converters
+   utils/api_helpers
+   utils/api_logger
+   utils/api_utils
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_webservices.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_webservices.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_webservices.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`webservices` -- Web Services Components Module
+***********************************************
+
+.. automodule:: waeup.kofa.webservices
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_widgets.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_widgets.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_widgets.rst	(revision 16045)
@@ -0,0 +1,22 @@
+`widgets` -- Special Widgets Package
+************************************
+
+(see also: :ref:`api-widgets-modules`)
+
+.. automodule:: waeup.kofa.widgets
+   :members:
+   :undoc-members:
+
+.. _api-widgets-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   widgets/api_datewidget
+   widgets/api_phonewidget
+   widgets/api_objectwidget
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/api_workflow.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`workflow` -- Workflow Components Module
+****************************************
+
+.. automodule:: waeup.kofa.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_applicant.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.applicant` - Applicant Components
+*********************************************
+
+.. automodule:: waeup.kofa.applicants.applicant
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_authentication.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.authentication` - Applicants Authentication Components
+******************************************************************
+
+.. automodule:: waeup.kofa.applicants.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_batching.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.batching` - Batch Importer for Applicants
+*****************************************************
+
+.. automodule:: waeup.kofa.applicants.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`applicants.browser` - Applicants UI Components
+***********************************************
+
+.. automodule:: waeup.kofa.applicants.browser
+   :members:
+
+   .. :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_container.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_container.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_container.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.container` - Container for Applicants
+*************************************************
+
+.. automodule:: waeup.kofa.applicants.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.interfaces` - Applicants Related Interfaces
+*******************************************************
+
+.. automodule:: waeup.kofa.applicants.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_permissions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.permissions` - Applicants Permission Components
+***********************************************************
+
+.. automodule:: waeup.kofa.applicants.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_root.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_root.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_root.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.root` - Root for Applicants Containers
+**************************************************
+
+.. automodule:: waeup.kofa.applicants.root
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`applicants.workflow` - Applicants Workflow Components
+******************************************************
+
+.. automodule:: waeup.kofa.applicants.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_async.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_async.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_async.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`browser.async` - Browser Job Components
+****************************************
+
+.. automodule:: waeup.kofa.browser.async
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_breadcrumbs.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`browser.breadcrumbs` - Breadcrumbs
+***********************************
+
+.. automodule:: waeup.kofa.browser.breadcrumbs
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_captcha.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_captcha.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_captcha.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.captcha` - Captcha Components
+**************************************
+
+.. automodule:: waeup.kofa.browser.captcha
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_exceptions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`browser.exceptions` - Browser Exceptions
+*****************************************
+
+.. automodule:: waeup.kofa.browser.exceptions
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_fileviewlets.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_fileviewlets.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_fileviewlets.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.fileviewlets` - File Viewlets
+**************************************
+
+.. automodule:: waeup.kofa.browser.fileviewlets
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.interfaces` - Browser Interfaces
+*****************************************
+
+.. automodule:: waeup.kofa.browser.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_layout.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_layout.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_layout.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`browser.layout` - Layout
+*************************
+
+.. automodule:: waeup.kofa.browser.layout
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pages.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pages.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pages.rst	(revision 16045)
@@ -0,0 +1,7 @@
+`browser.pages` - Pages (Views)
+*******************************
+
+.. automodule:: waeup.kofa.browser.pages
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pdf.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pdf.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_pdf.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.pdf` - PDF Views
+*************************
+
+.. automodule:: waeup.kofa.browser.pdf
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_reports.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_reports.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_reports.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.reports` - Reports Components
+**************************************
+
+.. automodule:: waeup.kofa.browser.reports
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_viewlets.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`browser.viewlets` - Viewlets
+*****************************
+
+.. automodule:: waeup.kofa.browser.viewlets
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 16045)
@@ -0,0 +1,11 @@
+`hostels.browser` - Hostel Browser Components
+*********************************************
+
+Probably due to a bug in Sphinx autodoc couldn't import/find some classes and
+reported error: "grokcore, please check your spelling and sys.path".
+Thus, automodule has been temporarily disabled.
+
+..
+  .. automodule:: waeup.kofa.hostels.browser
+     :members:
+     :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 16045)
@@ -0,0 +1,8 @@
+`hostels.catalog` - Hostel Catalog
+**********************************
+
+.. automodule:: waeup.kofa.hostels.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: BedIndexes
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_container.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_container.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_container.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`hostels.container` - Hostels Container Components
+**************************************************
+
+.. automodule:: waeup.kofa.hostels.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_hostel.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`hostels.hostel` - Hostel Components
+************************************
+
+.. automodule:: waeup.kofa.hostels.hostel
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`hostels.interfaces` - Hostel Related Interfaces
+************************************************
+
+.. automodule:: waeup.kofa.hostels.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_permissions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`hostels.permissions` - Hostel Permissions
+******************************************
+
+.. automodule:: waeup.kofa.hostels.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`hostels.vocabularies` - Hostel Vocabularies
+********************************************
+
+.. automodule:: waeup.kofa.hostels.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_image.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_image.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_image.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`image.image` - Image File Components
+*************************************
+
+.. automodule:: waeup.kofa.image.image
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_schema.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_schema.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/image/api_schema.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`image.schema` - Image File Schemas
+***********************************
+
+.. automodule:: waeup.kofa.image.schema
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/index.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/index.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/index.rst	(revision 16045)
@@ -0,0 +1,48 @@
+API
+***
+
+.. automodule:: waeup.kofa
+   :members:
+
+Modules and Packages
+--------------------
+
+`waeup.kofa` provides the following modules and subpackages:
+
+.. toctree::
+   :maxdepth: 1
+
+   api_accesscodes
+   api_app
+   api_applicants
+   api_async
+   api_authentication
+   api_beaker
+   api_browser
+   api_catalog
+   api_configuration
+   api_datacenter
+   api_hostels
+   api_image
+   api_imagestorage
+   api_interfaces
+   api_language
+   api_maintenance
+   api_mandates
+   api_meta
+   api_objecthistory
+   api_payments
+   api_permissions
+   api_reports
+   api_schema
+   api_schoolgrades
+   api_smtp
+   api_sourcefactory
+   api_startup
+   api_students
+   api_university
+   api_userscontainer
+   api_utils
+   api_webservices
+   api_widgets
+   api_workflow
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_browser.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`mandates.browser` - Mandates UI Components
+*******************************************
+
+.. automodule:: waeup.kofa.mandates.browser
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_container.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_container.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_container.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`mandates.container` - Mandate Container Components
+***************************************************
+
+.. automodule:: waeup.kofa.mandates.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_mandate.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_mandate.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/mandates/api_mandate.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`mandates.mandate` - Mandate Components
+***************************************
+
+.. automodule:: waeup.kofa.mandates.mandate
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_container.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_container.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_container.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`payments.container` - Payments Container Components
+****************************************************
+
+.. automodule:: waeup.kofa.payments.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`payments.interfaces` - Payment Interfaces
+******************************************
+
+.. automodule:: waeup.kofa.payments.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_payment.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_payment.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/payments/api_payment.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`payments.payment` - Payment Components
+***************************************
+
+.. automodule:: waeup.kofa.payments.payment
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/schema/api_field.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/schema/api_field.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/schema/api_field.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`schema.field` - Special Schema Fields
+**************************************
+
+.. automodule:: waeup.kofa.schema.field
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_accommodation.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.accommodation` - Student Accommodation Components
+***********************************************************
+
+.. automodule:: waeup.kofa.students.accommodation
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_authentication.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_authentication.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_authentication.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.authentication` - Student Authentication Components
+*************************************************************
+
+.. automodule:: waeup.kofa.students.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_batching.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_batching.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_batching.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.batching` - Student Import and Export Components
+**********************************************************
+
+.. automodule:: waeup.kofa.students.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_browser.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.browser` - Students UI Components
+*******************************************
+
+.. automodule:: waeup.kofa.students.browser
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_catalog.rst	(revision 16045)
@@ -0,0 +1,8 @@
+`students.catalog` - Students Catalog Components
+************************************************
+
+.. automodule:: waeup.kofa.students.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: StudentIndexes
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_container.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_container.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_container.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.container` - Students Container
+*****************************************
+
+.. automodule:: waeup.kofa.students.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_dynamicroles.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.dynamicroles` - Dynamic Roles Components
+**************************************************
+
+.. automodule:: waeup.kofa.students.dynamicroles
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.interfaces` - Students Related Interfaces
+***************************************************
+
+.. automodule:: waeup.kofa.students.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_payments.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_payments.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_payments.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.payments` - Student Payments
+**************************************
+
+.. automodule:: waeup.kofa.students.payments
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_permissions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_permissions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_permissions.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.permissions` - Student Permissions
+********************************************
+
+.. automodule:: waeup.kofa.students.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_student.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_student.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_student.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.student` - Student Components
+***************************************
+
+.. automodule:: waeup.kofa.students.student
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studycourse.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.studycourse` - Student Study Course Components
+********************************************************
+
+.. automodule:: waeup.kofa.students.studycourse
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studylevel.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.studylevel` - Student Study Level Components
+******************************************************
+
+.. automodule:: waeup.kofa.students.studylevel
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_utils.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_utils.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_utils.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.utils` - Student Utilities
+************************************
+
+.. automodule:: waeup.kofa.students.utils
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_viewlets.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 16045)
@@ -0,0 +1,11 @@
+`students.viewlets` - Student Browser Viewlets
+**********************************************
+
+Probably due to a bug in Sphinx autodoc couldn't import/find some classes and
+reported error: "grokcore, please check your spelling and sys.path".
+Thus, automodule has been temporarily disabled.
+
+..
+  .. automodule:: waeup.kofa.students.viewlets
+     :members:
+     :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.vocabularies` - Student Vocabularies
+**********************************************
+
+.. automodule:: waeup.kofa.students.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/students/api_workflow.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`students.workflow` - Student Workflow
+**************************************
+
+.. automodule:: waeup.kofa.students.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_batching.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_batching.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_batching.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.batching` - Importer Components
+*******************************************
+
+.. automodule:: waeup.kofa.university.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_catalog.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.catalog` - Cataloging Components
+********************************************
+
+.. automodule:: waeup.kofa.university.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificate.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificate.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificate.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.certificate` - Certificate Components
+*************************************************
+
+.. automodule:: waeup.kofa.university.certificate
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificatescontainer.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.certificatescontainer` - Certificates Container Components
+**********************************************************************
+
+.. automodule:: waeup.kofa.university.certificatescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_course.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_course.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_course.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.course` - Course Components
+***************************************
+
+.. automodule:: waeup.kofa.university.course
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_coursescontainer.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.coursescontainer` - Courses Container Components
+************************************************************
+
+.. automodule:: waeup.kofa.university.coursescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_department.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_department.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_department.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.department` - Department Components
+***********************************************
+
+.. automodule:: waeup.kofa.university.department
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_facultiescontainer.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.facultiescontainer` - Faculties Container Components
+****************************************************************
+
+.. automodule:: waeup.kofa.university.facultiescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_faculty.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_faculty.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_faculty.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.faculty` - Faculty Components
+*****************************************
+
+.. automodule:: waeup.kofa.university.faculty
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.interfaces` - Interfaces of the University Package
+**************************************************************
+
+.. automodule:: waeup.kofa.university.interfaces
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`university.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.kofa.university.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_batching.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_batching.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_batching.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`utils.batching` - Batching Components
+**************************************
+
+.. automodule:: waeup.kofa.utils.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_converters.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_converters.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_converters.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`utils.converters` - Converter Components
+*****************************************
+
+.. automodule:: waeup.kofa.utils.converters
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_helpers.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`utils.helpers` - Helper Components
+***********************************
+
+.. automodule:: waeup.kofa.utils.helpers
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_logger.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_logger.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_logger.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`utils.logger` - Logger Components
+**********************************
+
+.. automodule:: waeup.kofa.utils.logger
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_utils.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_utils.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/utils/api_utils.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`utils.utils` - Utilities
+*************************
+
+.. automodule:: waeup.kofa.utils.utils
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_datewidget.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`widgets.datewidget` - Date Widget
+**********************************
+
+.. automodule:: waeup.kofa.widgets.datewidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_objectwidget.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`widgets.objectwidget` - Object Widget
+**************************************
+
+.. automodule:: waeup.kofa.widgets.objectwidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_phonewidget.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 16045)
@@ -0,0 +1,6 @@
+`widgets.phonewidget` - Phone Widget
+************************************
+
+.. automodule:: waeup.kofa.widgets.phonewidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/developernotes.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/developernotes.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/developernotes.rst	(revision 16045)
@@ -0,0 +1,150 @@
+Developer Notes
+***************
+
+How to setup a developer instance of the WAeUP.Kofa, handle tests, docs
+etc.
+
+The new WAeUP.Kofa is based on `Grok <http://grok.zope.org/>`_.
+
+Installing a developer copy
+===========================
+
+See :ref:`installing_linux`
+
+Documentation
+=============
+
+With the :mod:`waeup.kofa` package we try to reach high standards in
+both, documentation and testing.
+
+:mod:`waeup.kofa` makes extensive use of doctests, which this way also
+become both: executable (i.e. testable) examples and documentation.
+
+Building documentation
+----------------------
+
+See :ref:`building_documentation`
+
+Writing documentation
+---------------------
+
+means explaining to other developers what your code does and test it
+at the same time. See the many .txt files in the :mod:`waeup.kofa`
+package for examples.
+
+Testing
+=======
+
+Tests are most important to the reliability of the :mod:`waeup.kofa`
+package. We don't tell someone that our code works, if we cannot prove
+it. And we prove it by testing.
+
+Running tests
+-------------
+
+You can run all the tests for the package by doing::
+
+  $ bin/test
+
+If you like colored output (can improve readability of failure
+messages), use the ``-c`` switch::
+
+  $ bin/test -c
+
+We have many tests in the :mod:`waeup.kofa` package so that sometimes
+you only want the functional *or* the unit tests to run. This can be
+done like this::
+
+  $ bin/test -c -u
+
+if you want only unit tests or like this::
+
+  $ bin/test -c -f
+
+if you only want functional tests.
+
+
+Writing tests
+-------------
+
+Is a wiiide topic. For now, please see the numerous .txt files in the
+source. Most of the **are** tests.
+
+Generally, we use `z3c.testsetup` for finding and setting up tests.
+
+There are mainly two kinds of tests we use:
+
+* unit- or simple doctests
+
+  These are included in test runs automatically, if they provide a
+  marker like this somewhere (normally near top of file)::
+
+    .. :doctest:
+
+  Most unit tests furthermore declare that they want to be run inside
+  the `WAeUPKofaUnitTestLayer` defined in `waeup.kofa.testing`. This
+  layer groks the whole `waeup.kofa` package, so that all ZCA
+  components are already setup when you start your tests.
+  
+  To declare that a unit test testfile should be run inside this
+  layer, the testfile has to provide the following line::
+
+    .. :layer: waeup.kofa.testing.WAeUPKofaUnitTestLayer
+
+  Use it, if in your tests you make use of registered components like
+  utilities, adapters and the like.
+
+* integration or functional tests
+
+  These provide a full-blown ZODB storage, so that we can emulate
+  browser requests to the whole system. Functional tests are much more
+  expensive in terms of memory and runtime but needed, if you want to
+  test UI components.
+
+  A testfile is registered as functional test on testruns when it
+  provides a line like the following::
+
+    :Test-Layer: functional
+
+
+Code coverage
+-------------
+
+We want to make sure, that all aspects of our software are
+tested. This means, that all parts of the codes should be tested
+somewhere.
+
+To tell how good our test coverage is, we can also use the testrunner
+(``bin/test``)::
+
+  $ bin/test --coverage=coverage
+
+will run the tests but also look, which parts of code were touched by
+them. For releases we want 100% coverage. Beware: running tests with
+the ``--coverage`` switch slows down tests by factor 10 or more.
+
+The command above will output a table with percentages. Furthermore,
+in ``/parts/test/coverage`` you will (after the testrun) find your
+sources preceeded by markers which tell, how often (or none) a certain
+line was used in tests.
+
+To have a more convenient cmdline interface, we also provide some
+shortcuts::
+
+  $ bin/coverage-detect
+
+will run all the tests as shown above and put the results in
+``parts/coverage-detect/coverage``.
+
+After that you can run::
+
+  $ bin/coveragereport
+
+to get a browsable HTML representation of test coverage in
+``coverage-report/`` subdir.
+
+Both, the coverage reports and HTML documentation generated by sphinx
+can be packed and put onto a website as-is.
+
+It is also possible to generate the docs and reports nightly by a
+buildbot or something like this.
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/index.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/index.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/index.rst	(revision 16045)
@@ -0,0 +1,11 @@
+.. _developer_handbook:
+
+Developer Handbook
+******************
+
+.. toctree::
+   :maxdepth: 2
+   
+   developernotes
+   api/index
+   profiling
Index: main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/profiling.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/profiling.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/developerdocs/profiling.rst	(revision 16045)
@@ -0,0 +1,376 @@
+Profiling
+*********
+
+*or: What Did My Application Do in the Afternoon?*
+
+This how-to explains how you can profile your Grok application or
+Grok/Zope itself. This way you get an idea of the bottlenecks of your
+application or in other words: where it spends its time.
+
+.. note:: This text is an updated version of my how-to as it can seen
+          on the old Grok site (no, I won't give a link to that
+          slightly outdated stuff).
+
+Profiling with Grok
+===================
+
+When it comes to a web framework like Grok, profiling is not as easy
+as with commandline or desktop applications. You normally want to
+trigger certain requests and see, in which parts of your code how much
+time or memory was spent.
+
+Here is how you can do this.
+
+Prerequisites
+=============
+
+We assume you know how to create a Grok project (your application) and
+have one already at hand.
+
+Installing a profiler
+=====================
+
+There are some profiling tools available with every Python
+installation. Usually, they are started together with some desktop- or
+commandline application, wrapping it in a way to measure internal calls
+and other things interesting for developers.
+
+With web-frameworks, however, we often only want to check certain
+requests. Luckily, with `Paste`_ and `WSGI`_ we have a mechanism, that
+can serve in a similar way: we can define a (WSGI_) pipeline of apps
+and filters that wrap each other, similar to the approach of regular
+non-web applications. We also have a profiler already available, that
+*is* some WSGI_ application itself and can profile things when some
+HTTP request comes in: `repoze.profile`_ is even configurable over a
+web frontend.
+
+Install repoze.profile_
+=======================
+
+In the ``buildout.cfg`` of your project add the repoze.profile_ egg to list
+of eggs of your application. Look out for a section named ``[app]``, which
+could read like this or similar::
+
+
+  ...
+  [app]
+  recipe = zc.recipe.egg
+  eggs = cptapp
+         z3c.evalexception>=2.0
+         Paste
+         PasteScript
+         PasteDeploy
+         repoze.profile
+  interpreter = python-console
+  ...
+
+Here the added ``repoze.profile`` stanza is important.
+
+Now run::
+
+  $ bin/buildout
+
+to fetch the egg from the net if it is not already available and to
+make it known to the generated scripts.
+
+Create a ``profiler.ini``
+=========================
+
+To make use of the new egg we must tell paster about it. This is done
+by an appropriate initialization file we create now::
+
+  # file: profiler.ini
+  [app:myapp]
+  use = egg:sample
+
+  [filter:myprofiler]
+  use = egg:repoze.profile#profile
+
+  [pipeline:main]
+  pipeline = myprofiler myapp
+
+  [server:main]
+  use = egg:Paste#http
+  host = 127.0.0.1
+  port = 8080
+
+  [DEFAULT]
+  # set the name of the zope.conf file
+  zope_conf = %(here)s/zope.conf
+
+Here we tell paster (the HTTP server) in the ``pipeline`` to filter
+each web request passing it first to repoze.profile_ which then calls
+our own ``sample`` application. This way the profiler can start its
+job and measure all calls done by the following application call.
+
+It is crucial, that you use the name of *your* project egg here in the
+``[app:myapp]`` section. In the sample config above we assume that the
+project (application) is called ``sample``, so that the egg with our
+application is called the same.
+
+.. note:: modern Grok projects put their paster configuration files as
+          *templates* into the local ``etc/`` directory and do not
+          call them ``debug.ini``, ``profile.ini``, etc. but
+          ``debug.ini.in``, ``profile.ini.in``, etc (note the trailing
+          ``.in``). This is not covered here.
+
+Put this new file in the same directory as where your ``zope.conf``
+lives (*not* ``zope.conf.in``). For newer projects this is
+``parts/etc/``.
+
+Start Profiling
+===============
+
+With the given setup we can start profiling by::
+
+  $ bin/paster serve parts/etc/profiler.ini
+
+If your ``profiler.ini`` file resides elsewhere, you must use a
+different location, of course.
+
+.. note:: you dont't *have* to create a separate profiler config. You
+          can, of course, update an existing ``deploy.ini`` or
+          ``debug.ini``. But as profiling takes more time than calls
+          without profiling, it is a good idea to enable profiling
+          only on request, i.e. when starting paster with a
+          respective configuration.
+
+The server will start as usual and you can do everything you like with
+it.
+
+Browsing the Profiler
+=====================
+
+To get to the profiler, enter the following URL:
+
+    http://localhost:8080/__profile__
+
+This brings us to the profiler web frontend. If you have browsed your
+instance before, you will get some values about the timings of last
+requests. If not, then browse a bit over your application to collect
+some data. The data is collected 'in background' during each request
+and added to the values already collected.
+
+The result might look like this:
+
+.. image:: screenshot01.png
+
+As you can see, there are a few options you can play with. You can
+filter the results by name, sort it by time or other stats, enable
+display of full paths, etc.
+
+When you stop paster, all collected data is removed. On the next start
+you can generate new one.
+
+Profiling a certain view
+========================
+
+Say we want to profile the performance of the ``index`` view created
+by the our application. To do this, we first have to install an
+instance of our application. Let's call it simply ``app``.
+
+So, create an instance of your application under the name ``app`` in
+the grok UI admin interface.
+
+Now we can access
+
+    http://localhost:8080/app
+
+and the usual index page will (hopefully) appear.
+
+If we go back to the profiler, however, we will see the summed up
+values of *all* requests we did up to now - including all the actions
+in the admin interface etc. we are not interested in.
+
+We therefore clear the current data by clicking on ``clear`` on the
+profiler page.
+
+Now we access the page we want to examine directly and go to the above
+URL directly.
+
+When we now go back to the profiler, we only see the values of the
+last request. That's the data we are interested in.
+
+Profiling mass requests
+=======================
+
+Very often a single request to a view does not give us reliable data:
+too many factors can influence the request to make its values not very
+representative. What we often want are *many* requests and the average
+values appearing here.
+
+This means for our view: we want to do several hundreds requests to
+the same view. But as we are lazy, we don't want to press the reload
+button several hundred or even thousand times. Luckily there are tools
+available, which can do that for us.
+
+One of this tools is the apache benchmarking tool ``ab`` from the
+Apache project. On Ubuntu systems it is automatically installed, if
+you have the apache webserver installed.
+
+With ``ab`` (apache benchmarking) we can trigger 1,000 requests to our
+index page with one command::
+
+  $ ab -n1000 -c4 http://127.0.0.1/app/@@index
+
+This will give us 1,000 requests, of which at most four are triggered
+concurrently, to the URL http://127.0.0.1/app/@@index. Please don't do
+this on foreign machines!
+
+The result might look like this::
+
+  Benchmarking 127.0.0.1 (be patient)
+  Completed 100 requests
+  Completed 200 requests
+  Completed 300 requests
+  Completed 400 requests
+  Completed 500 requests
+  Completed 600 requests
+  Completed 700 requests
+  Completed 800 requests
+  Completed 900 requests
+  Finished 1000 requests
+  
+  
+  Server Software:        PasteWSGIServer/0.5
+  Server Hostname:        127.0.0.1
+  Server Port:            8080
+  
+  Document Path:          /app/@@index
+  Document Length:        198 bytes
+  
+  Concurrency Level:      4
+  Time taken for tests:   38.297797 seconds
+  Complete requests:      1000
+  Failed requests:        0
+  Write errors:           0
+  Total transferred:      448000 bytes
+  HTML transferred:       198000 bytes
+  Requests per second:    26.11 [#/sec] (mean)
+  Time per request:       153.191 [ms] (mean)
+  Time per request:       38.298 [ms] (mean, across all concurrent requests)
+  Transfer rate:          11.41 [Kbytes/sec] received
+
+  Connection Times (ms)
+                min  mean[+/-sd] median   max
+  Connect:        0    0   0.0      0       0
+  Processing:    94  152  17.3    151     232
+  Waiting:       86  151  17.3    150     231
+  Total:         94  152  17.3    151     232
+
+  Percentage of the requests served within a certain time (ms)
+    50%    151
+    66%    153
+    75%    156
+    80%    158
+    90%    176
+    95%    189
+    98%    203
+    99%    215
+   100%    232 (longest request)
+
+Also this benchmarking results can be interesting. But we want to know
+more about the functions called during this mass request and how much
+time they spent each. This can be seen, if we now go back to the
+browser and open
+
+    http://localhost:8080/__profile__
+
+again.
+
+.. note:: ``ab``, of course, is a simple, rude method for stress
+          testing. Although it provides many options, you might want
+          to look for more sophisticated methods to generate tons of
+          requests. ``multi-mechanize`` is such a tool that can kill
+          your server with the same power as ``ab`` but does it
+          smarter.
+
+Turning the Data into a Graph
+=============================
+
+All this is very nice, but sometimes a picture tells more than
+thousand words. So let's turn all this data into some graph.
+
+As repoze.profile_ cannot do this for us, we have to 'export' the
+collected data first.
+
+Exporting Profiling Data
+========================
+
+The web frontend provided by repoze.profile_ is very nice for
+analyzing ad-hoc. But sometimes we want to have the data 'exported' to
+process it further with other tools or simply archiving the results.
+
+Luckily we can do so by grabbing the file ``wsgi.prof`` which contains
+all the data presented in the web interface. This file is created by
+repoze.profile_ while working and in the top of the project
+directory. In fact the profiler stores all collected data in that
+``wsgi.prof`` file also for own usage.
+
+Be careful: when you click ``clear`` in the webinterface, then the
+file will vanish. Also stopping ``paster`` will make it disappear. So
+copy it to some secure location where we can process the data further
+while the web server is still running (and after you did all requests
+you want to analyze).
+
+Because repoze.profile_ makes use of the standard Python profiler in
+the :mod:`profile` or :mod:`cProfile` module, the data in the
+``wsgi.prof`` file conforms to output generated by these profilers.
+
+Converting the Data into dot-format
+===================================
+
+One of the more advanced tools to create graphs from profiling
+information is ``dot`` from graphviz_. To make use of it, we first
+have to convert the data in ``wsgi.prof`` file into something
+``dot``-compatible.
+
+There is a tool available, which can do the job for us, a Python
+script named ``GProf2Dot`` which is available at:
+
+    http://code.google.com/p/jrfonseca/wiki/Gprof2Dot
+
+Download the script from:
+
+    http://jrfonseca.googlecode.com/svn/trunk/gprof2dot/gprof2dot.py
+
+We can now turn our profiling data into a ``dot`` graph by doing::
+
+  $ python grprof2dot.py -f pstats -o wsgi.prof.dot wsgi.prof
+
+This will turn our input file ``wsgi.prof`` of format `pstats`
+(=Python stats) into a ``dot``-file named ``wsgi.prof.dot``.
+
+Converting the dot file into Graphics
+=====================================
+
+Now we can do the last step and turn our dot file into a nice graphics
+file. For this we need of course the dot programme, which on Ubuntu
+systems can be easily installed doing::
+
+  $ sudo apt-get install dot
+
+Afterwards we do the final transformation by::
+
+  $ dot -Tpng -omygraph.png wsgi.prof.dot
+
+This will generate a PNG file where we can see in terms of graphs
+where a request (or thousands thereof) spends most of the
+time. ``dot`` can generate **huge** graphs. A tiny snippet of a sample
+is shown below.
+
+.. image:: screenshot02.png
+
+All the used tools (``ab``, ``dot``, ``gprof2dot``) provide a huge
+bunch of options you might want to explore further. This way we can
+generate more or less complete graphs (leaving out functions of little
+impact), coulours etc.
+
+In the end you hopefully know more about your application and where it
+spent its time.
+
+.. _`repoze.profile`: http://pypi.python.org/pypi/repoze.profile
+.. _`Paste`: http://pythonpaste.org/
+.. _`WSGI`: http://www.python.org/peps/pep-0333.html
+.. _`graphviz`: http://www.graphviz.org/
+.. _`GProf2Dot`: http://code.google.com/p/jrfonseca/wiki/Gprof2Dot
Index: main/waeup.kofa/branches/uli-py3/docs/source/documentation.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/documentation.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/documentation.rst	(revision 16045)
@@ -0,0 +1,26 @@
+About these documents
+*********************
+
+These documents are generated from `reStructuredText
+<http://docutils.sf.net/rst.html>`_ sources by `Sphinx`_, an excellent
+document processor specifically written for the Python documentation
+by Georg Brandl and contributors.
+
+Development of this documentation is organized by Uli Fouquet (``uli
+at waeup dot org``) and Henrik Bettermann (``henrik at waeup dot
+org``). We're always looking for volunteers wanting to help with the
+docs, so feel free to send a mail there!
+
+Many thanks go to:
+
+* the `docutils <http://docutils.sf.net/>`_ project for creating
+  reStructuredText and the docutils suite;
+* Georg Brandl for his `sphinx` package.
+
+See :ref:`building_documentation` to learn how to create HTML or PDF
+versions of this docs.
+
+See :ref:`reporting-bugs` for information how to report bugs in
+`waeup.kofa` itself.
+
+.. _Sphinx: http://sphinx.org/
Index: main/waeup.kofa/branches/uli-py3/docs/source/glossary.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/glossary.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/glossary.rst	(revision 16045)
@@ -0,0 +1,16 @@
+.. _glossary:
+
+Glossary
+********
+
+.. glossary::
+
+   Kofa
+     Student Management System
+
+   WAeUP
+     West African eUniversity Project
+
+   waeup.kofa
+     A Python package explained in this documentation
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/index.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/index.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/index.rst	(revision 16045)
@@ -0,0 +1,11 @@
+.. WAeUP Student Management System (Kofa) documentation master file, created by
+   sphinx-quickstart on Thu Jul 29 10:41:59 2010.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   contents
Index: main/waeup.kofa/branches/uli-py3/docs/source/intro.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/intro.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/intro.rst	(revision 16045)
@@ -0,0 +1,1 @@
+.. include:: ../../README.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/license.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/license.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/license.rst	(revision 16045)
@@ -0,0 +1,11 @@
+.. _license:
+
+***********
+The License
+***********
+
+`waeup.kofa` is covered by the following license. If that license
+does not fit your needs and you need a copy of the software licensed
+differently, please contact ``henrik <at> waeup <dot> org``.
+
+.. literalinclude:: ../../LICENSE
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/academics.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/academics.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/academics.rst	(revision 16045)
@@ -0,0 +1,194 @@
+.. _academic_section:
+
+Academic Section 
+****************
+
+Academic bodies of universities are hierarchically structured. A
+university contains faculties which again host departments. The
+latter offer courses (course units) and study programmes (courses of
+study). A course is a series of classes or lessons on a particular
+subject. This can be a lecture, tutorial, seminar, practical work
+offered by a lecturer of the department over a period of an academic
+term. A course can also be only an exam at the end of an academic
+term. A study programme is defined by a set of course units to be
+taken at various programme levels and by an academic degree
+certificate, which is issued at the end of the programme.
+Information about universities, their study programmes, lecturers
+and courses can be found in the university prospectus.
+
+Kofa's academic section takes on the task of the university
+prospectus. It is build to disseminate information about faculties,
+departments, courses, study programmes and the sets of courses to be
+taken. The academic section reflects exactly the universities'
+hierarchical structure as described above. Technically speaking, the
+academic section is a container of type `FacultiesContainer` with id
+``academics`` which contains faculty subcontainers. They again
+contain department subobjects and so on. This leads to a
+:ref:`treelike storage of objects <object_database>`::
+
+  Academic Section (FacultiesContainer)
+  |
+  +---> Faculty
+        |
+        +---> Department
+              |
+              +---> CoursesContainer
+              |     |
+              |     +---------------------> Course
+              |                             ^
+              +---> CertificatesContainer   |
+                    |                       |
+                    +-----> Certificate     |
+                            |               |
+                            +-----> CertificateCourse
+
+
+Faculty
+=======
+
+Faculties are container objects of type `Faculty`. They have a
+`code`, a `title` a `title_prefix` and two officer name attributes:
+`officer_1` and `officer_2`. Officer names are not made use of in
+the base package. They can be used in custom packages to 'sign' pdf
+slips and reports. Usually, `officer_1` is reserved for the Dean of
+Faculty. Furthermore, faculties and departments have a `longtitle`
+property which :py:func:`composes
+<waeup.kofa.university.faculty.longtitle>` and returns the full
+title as it appears in tables, in breadcrumbs and in many other
+places:
+
+  `longtitle` = `title_prefix` + `title` (`code`)
+
+The following :ref:`local roles <local_roles>` can be assigned at
+faculty tree level:
+
+  .. autoattribute:: waeup.kofa.university.faculty.Faculty.local_roles
+     :noindex:
+
+
+Department
+==========
+
+Additionally, each department object has the attributes
+`certificates` and `courses`. These attributes again are containers
+which hold certificate and course objects respectively. A
+:py:meth:`traverse
+<waeup.kofa.university.department.Department.traverse>` method leads
+the user to believe that `certificates` and `courses` are the ids of
+objects stored inside a department container. De facto they are not.
+However, it doesn't matter if we store a subobject 'in' or 'at' a
+parent object. The result is exactly the same: :ref:`a hierarchical,
+treelike storage of objects <object_database>` (see also diagram
+above).
+
+Department objects have four officer name attributes. Needless to say,
+`officer_1` should contain the name of the Head of Department. The
+title of the name fields can be localized in custom packages.
+
+The following :ref:`local roles <local_roles>` can be assigned at
+department tree level:
+
+  .. autoattribute:: waeup.kofa.university.department.Department.local_roles
+     :noindex:
+
+Course
+======
+
+The `Course` class inherits from `grok.Model` which means it is
+designed as a pure model and not a container. Courses are tips of
+the database tree branches. Course objects contain information as
+they are offered by the department. In the base package these are:
+`code`, `title`, `credits`, `passmark`, `semester` and the boolean
+attribute `former_course`. Like all objects in the academic section,
+they also have a `longtitle` property. A former course is an
+archived course which had been offered in the past and is no longer
+part of any curriculum.
+
+.. note::
+
+  Only the manager of the department, which offers the course,
+  decides how many credits can be earned, which pass mark must be
+  achieved and in which semester the course can be taken. The
+  manager cannot decide whether a course is mandatory or not, or
+  at which study level the course has to be taken when studying a
+  certain programme. This information is stored in
+  `CertificateCourse` objects, see below. Therefore it does not
+  make sense to speak e.g. of a 300 level course. Also course
+  codes like ``AEE311``, which tell the student that the course
+  is primarily intended for 300 level students, is somehow
+  misleading. There might be other study programmes which
+  recommend to take this course earlier or later.
+
+
+The following :ref:`local roles <local_roles>` can be assigned at
+course tree level:
+
+  .. autoattribute:: waeup.kofa.university.course.Course.local_roles
+   :noindex:
+
+.. _certificate:
+
+Certificate
+===========
+
+.. seealso::
+
+   :ref:`Certificates and Certificate Courses Doctests <certcourse_txt>`
+
+In Kofa, the terms 'certificate' and 'study programme' are used
+synonymously. A certificate object holds information about the study
+programme. Important data for further processing in Kofa are: the
+various school fees to be paid at certain levels, programme start
+and end level, the study mode and, last but not least, the
+application category to which the programme belongs.
+
+Certificates are containers which contain `CertificateCourse`
+objects. That means a certificate defines the curriculum, i.e. the
+list of course units which have to or can be taken within the
+programme.
+
+The following :ref:`local roles <local_roles>` can be assigned at
+certificate tree level:
+
+  .. autoattribute:: waeup.kofa.university.certificate.Certificate.local_roles
+   :noindex:
+
+.. warning::
+
+  Do not remove certificates without backing up the student data
+  of this department. If a certificate or even one of its parent
+  containers is removed, students studying this programme are
+  'homeless' and need to be re-allocated to another study
+  programme.
+
+
+Certificate Course
+==================
+
+`CertificateCourse` objects point to `Course` objects which means
+they have a `course` attribute which is one of the course objects
+stored in the portal. If the course is removed, an event handler
+takes care of deleting also all referring certificate courses. The
+same happens if a course is beeing marked as former course (see
+above). A former course cannot be part of a curriculum.
+
+Certificate courses have three more attributes: `level` (integer),
+`mandatory` (boolean) and `course_category` (choice). Simply put,
+certificate courses carry the information at which level a certain
+course can or has to be taken to meet the current curriculum. Course
+categories are not available in the base package but can easily be
+defined in custom packages. Some universities distinguish e.g. core,
+required and elective courses and need this information in reports.
+
+No local role can be assigned at certificate course tree level.
+
+Browser Pages
+=============
+
+In the user handbook we do not describe how to browse the academic
+section. The menu navigation is self-explanatory and it's
+quite easy to follow the menu prompts. However, in the beginning of
+the development of Kofa, we used extensive doc tests which describe
+the navigation very well. Thus navigating through the academic
+section and other parts of Kofa with a test browser is perfectly
+described in :ref:`pages.txt <pages_txt>`.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/accesscodes.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/accesscodes.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/accesscodes.rst	(revision 16045)
@@ -0,0 +1,4 @@
+.. _accesscodes:
+
+Access Codes :sup:`in progress`
+*******************************
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants.rst	(revision 16045)
@@ -0,0 +1,93 @@
+.. _applicants_section:
+
+Applicants Section
+******************
+
+Applicants and students are different entities and stored in
+different places. Moreover, the students section is completely
+independent from the applicants section. Kofa can be configured
+without its `applicants` module but not without its `students`
+module.
+
+Anonymous users can self-register and automatically become
+applicants (users) of the portal. The number of applicants is not
+limited and, theoretically, Kofa can host hundredthousands of
+applicants in various applicants containers. Usually, only a few of
+them are beeing accepted for studying at the university. Once the
+application process has ended, admitted applicants are transformed
+into student records. New student records are created and the data
+are copied from the applicant records. When this 'student creation'
+process is finished, the applicant records are dispensable. Aim was
+to provide an easy way to get rid of the huge batches of applicant
+records by only one click, in order to keep the database clean and
+tidy. The process is described further below.
+
+.. note::
+
+  The term 'application' has a double meaning in English. An
+  'application' in computer sciences stands for a software or web
+  application. Software developers may get confused by using the term
+  in a different context. To take this into account, we prefer to
+  speak of 'applicant' in this documentation, instead of 'application'
+  although, in the linguistic context, it sometimes makes more sense
+  to speak of the latter. However, in the user interface (UI) and also
+  in Python docstrings the term 'application' predominates.
+
+Much like students, applicants are also users of the portal and
+the applicant object is a user account surrogate, see
+:ref:`Applicants and Students <applicants_and_students>`. In the
+following we always refer to the applicant container when talking
+about an applicant.
+
+In contrast to student containers, applicant containers do not
+contain subcontainers. The only objects they contain are applicant
+payment tickets (i.e. instances of `ApplicantOnlinePayment`). Thus
+the hierarchy is flat and not really treelike as the students
+section. Also, applicants (applicant containers) are stored in
+different parent containers (instances of `IApplicantsContainer`).
+There is not one single parent container like in the students
+section but one container for each application session and
+application type::
+
+  Applicants Section (ApplicantsRoot)
+  |
+  +---> ApplicantsContainer
+        |
+        +---> Applicant
+              |
+              +-----> ApplicantOnlinePayment
+
+
+Interfaces
+==========
+
+.. toctree::
+   :maxdepth: 3
+
+   applicants/interfaces
+
+Workflow & History
+==================
+
+.. toctree::
+   :maxdepth: 3
+
+   applicants/workflow
+
+Browser Pages
+=============
+
+.. toctree::
+   :maxdepth: 3
+
+   applicants/browser
+
+
+Student Record Creation
+=======================
+
+.. toctree::
+   :maxdepth: 3
+
+   applicants/create_students
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/browser.rst	(revision 16045)
@@ -0,0 +1,128 @@
+.. _applicant_registration:
+
+Registration
+============
+
+The `ApplicantRegistrationPage` allows to register in create or in
+update mode, depending on the
+:ref:`mode of its context<application_mode>`, i.e. the applicants
+container.
+
+In create mode, `firstname`, `middlename`, `lastname`, `email` and
+`phone` are requested. An unused record is taken, or a new
+record is being created if no unused record is found (see
+:ref:`description of application modes <application_mode>`).
+
+In update mode, only `reg_number`, `lastname` and `email` have to be
+entered. Kofa searches for an applicant record with matching
+registration number and lowercased lastname. If the search does not
+yield a result, the flash message: 'No application record found' is
+returned. The anonymous user will not know, if the registration
+number does not exist or the provided lastname does not match.
+Another ``if`` statement checks whether the password has already
+been set and used, i.e. the application has already been
+started. If so, the anonymous user can't register again and is being
+requested to proceed to the login page.
+
+In both registration modes a randomly generated password is set and
+the email address is saved. An email with login credentials is sent
+to this address. Finally, the browser is redirected to a landing
+page. Depending on the portals configuration, the landing page tells
+the user that an email has been send to her/his mailbox, or even
+discloses additionally the login credentials. The disclosure of
+credentials has two substantial drawbacks: (1) The login credentials
+can be misused by web crawlers for bulk account creation, which may
+cause the system to crash. (2) The email address provided by the
+user is not being verified and there is no guarantee that the
+address belongs to the user or that a mailbox with such an address
+exists. Therefore, we strongly recommend to only send credentials to
+email addresses.
+
+
+.. _container_maintenance:
+
+Preparation and Maintenance of Applicants Containers
+====================================================
+
+As described in the :ref:`interfaces chapter <application_mode>`,
+applicants containers are aware of their application mode. In update
+mode, containers must be pre-filled by import with application
+records from an external board. In create mode, the container can
+remain empty. Each time an applicant registers, a new record is
+being created and a corresponding user account set up. This is the
+method of choice, if the number of expected applicants is not very
+high (e.g. less than 500). Since each single account creation causes
+a significant growth of Kofa's database, we strongly recommend to
+pre-fill applicants containers with empty application records, if
+the number of expected applicants is higher. Pre-filling is done in
+a single transaction and does thus save database volume when
+application is ongoing.
+
+During application, many records are being initialized but not used.
+For various reasons, many applicants do create one or more
+application records with corresponding user accounts but fail to log
+in. These records remain in state ``initialized`` and are never
+turned to state ``started``. After a while, these records can be
+safely removed. When purging an applicants container, also all
+unused pre-filled application records are being removed. If
+necessary, the container must be pre-filled with empty records again.
+
+
+.. _application_form_locking:
+
+Form Locking
+============
+
+We mentioned regular :ref:`page_locking` mechanisms. The
+`ApplicantEditFormPage` has two additional locks. One is the same
+named applicant attribute `locked`. Applicants can only enter the
+edit page if their record is 'unlocked'. Locking and unlocking is
+automatically done by workflow event handlers. By default, the
+record is unlocked. Only when the applicant submits the record, it
+is being locked, which means the attribute is set ``True`` and the
+data can no longer be edited.
+
+The reader may wonder why Kofa is not using the workflow state
+instead. The additional locking mechanism allows officers to unlock
+and lock forms without triggering workflow transitions. A transition
+is always a major, and sometimes inappropriate intervention which is
+also recorded in the application history.
+
+Use case: An applicant has made a mistake and requests a change of
+submitted data. An officer accepts the change, temporarily unlocks
+the form to allow editing the data. Unlocking and re-locking is
+logged in ``applicants.log`` but not shown on pages or the
+application slip.
+
+The second lock is induced by the application deadline. If the
+application period has expired and the applicants container's
+`strict_deadline` attribute is set, the applicant is also not
+allowed to edit or even submit the form.
+
+.. note::
+
+  A locked-out applicant can still login and access the display pages
+  of the recod and also download payment and application slips. To
+  expell an applicant from the portal, the account has to be suspended
+  by setting the same-named attribute.
+
+
+.. _applicant_payment_tickets:
+
+Payment
+=======
+
+In contrast to the students section, there is no
+`PaymentsManageFormPage` to handle payment tickets separately.
+Payment tickets can be viewed, added and removed directly on the
+applicant manage and edit form pages. Officers can remove all
+payment tickets, applicants only those without a response code
+(`r_code`) and, if the form is unlocked, so that they are allowed to
+edit their data.
+
+As already mentioned in the workflow chapter, making a payment and
+redeeming a payment is done in one step. When the payment was
+successful or has been approved, also the applicant is automatically
+set to state ``paid``. There is no need to redeem the ticket
+manually.
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/create_students.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/create_students.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/create_students.rst	(revision 16045)
@@ -0,0 +1,104 @@
+.. _creating_students:
+
+Single Record Creation vs. Mass Creation
+========================================
+
+As brought up in the :ref:`application_workflow` chapter, the final
+step of the application process is the creation of the student
+record on the basis of the application data. Only applicants in
+state ``admitted`` can be turned into students. This is possible at
+various application section levels:
+
+1. Officers can create student records from single applicant
+   records. A link button appears on the display page of each admitted
+   applicant which directly triggers the creation process:
+
+  |create_student_button|
+
+2. The `ApplicantsContainerManageFormPage` provides a form button
+   which allows to create student records from above selected
+   applicants (maximum of 10):
+
+  |create_selected_students_button|
+
+  Only admitted applicants will be processed. Other selected
+  applicants will be skipped without further notice.
+
+3. Last but not least, the portal managers do see a link button:
+
+  |create_students_button|
+
+  on the `ApplicantsContainerPage` and on the `ApplicantsRootPage`
+  which creates student records from applicants inside a single
+  applicants container or even portal-wide respectively. A warning
+  message appears before the process is started. After start the
+  portal is immediately switched into maintenance mode to ensure that
+  nobody can enter the portal during record creation except for user
+  'admin'. Maintenance mode is switched off as soon as the process has
+  finished.
+
+.. note::
+
+  Batch creation of thousands or even tenthousands of student records
+  is very computation intensive and takes quite a long time, even with
+  powerful computers. Experience has shown that the portal must be
+  switched to maintenance mode (or even disconnected from the Internet)
+  in order to avoid database write conflict errors, which cannot be
+  resolved if also students access the portal. Usually conflict
+  errrors are not serious. Unresolved conflicts just lead to an error
+  message and the database remains unchanged. Unfortunataly, student
+  creation does not only alter the object database. As we will see
+  below, also folders and files in the filesystem are added. These
+  filesystem changes are not being reverted after unresolved conflicts,
+  a fact which can cause serious problems.
+
+
+The `createStudent` Method
+==========================
+
+The `Applicant.createStudent` method is called for each admitted
+applicant. The method performs a series of checks before it creates
+a new student record:
+
+1. Is the applicant really in the correct state?
+
+2. Has the registration number not been assigned to another student?
+
+3. Is a certificate (`course_admitted`) provided?
+
+4. Can all required student attributes be copied and set? The
+   following attributes are copied:
+
+   .. autoattribute:: waeup.kofa.applicants.applicant.Applicant.applicant_student_mapping
+      :noindex:
+
+5. Can the application slip be created? Defective images may
+   cause the adapter method to raise an exception (``IOError``).
+
+If all these checks are passed, a new student record is created, a
+password is set, the transitions ``create`` and ``admit`` are fired
+and all student study course attributes are set. All this is done
+in one database transaction.
+
+Furthermore, `createStudent` also produces an application slip pdf
+file, creates a new folder for the student in the filesystem and
+copies the pdf file together with the passport jpeg image file into
+the newly created folder. The copied application slip ensures, that
+the original application data do not get lost, when the application
+section is being purged.
+
+As mentioned above, folder creation and file copying are not part of
+the transaction and are not being rolled back if the database
+transactions fails, for example due to write conflict errors.
+Therefore, batch student creation should only be performed if the
+portal is offline.
+
+
+.. |create_student_button| image:: ../images/create_student_button.png
+   :scale: 50 %
+
+.. |create_students_button| image:: ../images/create_students_button.png
+   :scale: 50 %
+
+.. |create_selected_students_button| image:: ../images/create_selected_students_button.png
+   :scale: 50 %
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/interfaces.rst	(revision 16045)
@@ -0,0 +1,135 @@
+.. _applicants_interfaces:
+
+Parent Containers Interfaces
+============================
+
+`IApplicantsRoot`
+-----------------
+
+Much like ``academics`` and ``students``, also ``applicants`` is a
+unique container (of type `ApplicantsRoot`) located in the
+`IUniversity` instance. It is the counterpart of the students
+(section) container. The root container has a `description` schema
+field attribute which contains human readable, multi-lingual
+information about the application procedure in HTML format. This
+description can be seen by anonymous users, when they enter the
+applicants section (by pressing the 'Application' tab in the
+navigation bar).
+
+.. _multilingual:
+
+.. note::
+
+  A multi-lingual or localized text is a sequence of human-readable
+  text strings in different languages. The languages must be separated
+  by ``>>xy<<``, whereas ``xy`` is the language code. Text parts
+  without correct leading language separator - usually the first part
+  has no language descriptor - are interpreted as texts in the
+  portal's language. The text strings can be either HTML or
+  reStructuredText (REST) formatted.
+
+.. literalinclude:: ../../../../src/waeup/kofa/applicants/interfaces.py
+   :pyobject: IApplicantsRoot
+
+`IApplicantsContainer`
+----------------------
+
+The applicants root contains the various `ApplicantsContainer`
+objects and is also a configuration object.
+
+.. literalinclude:: ../../../../src/waeup/kofa/applicants/interfaces.py
+   :pyobject: IApplicantsContainer
+
+`statistics` and `expired` are read-only property attributes.
+`description_dict` contains the same information as the
+`description`, but the sequence of language translations has been
+split up and copied into a dictionary for faster processing. 
+
+Crucial for application are the `prefix`, `year`, `mode` and
+`application_category` schema fields. The `prefix` atribute can only
+be set when adding a container. It cannot be edited afterwards. The
+attribute determines the application type and automatically sets the
+prefix of the container code and of all applicant ids inside the
+container. The prefix is supplied by the `ApplicationTypeSource`
+(see `application types of base package
+<https://kofa-demo.waeup.org/sources#collapseAppTypes>`_). It is
+followed by the year of entry. Thus, the identifiers of applicants
+containers and the applicants inside the containers start with the
+same prefix-year sequence. Example: ``app2015`` which translates
+into 'General Studies 2015/2016'. Consequently, **applicants cannot
+be moved from one container to another.**
+
+.. _application_mode:
+
+The application mode is either ``create`` or ``update``. In **create
+mode** the container can be either left empty or it can be
+pre-filled with fresh and 'unused' records. In the first case,
+records are being created after submission of the first form. In the
+second case, unused record are fetched and filled with the form
+data. In both 'create mode' cases, applicants are requested to
+provide all data needed, including their name details. In **update
+mode**, the applicants container must have been pre-filled by import,
+e.g. with records provided by an external board. These records are
+called 'used' since they already contain data. Applicants can't
+create new records in update mode, they can only open and take
+possession of existing records.
+
+The application category is supplied by the
+`ApplicationCategorySource` (see `application categories of base
+package <https://kofa-demo.waeup.org/sources#collapseAppCats>`_) and
+refers to a group of study programmes (certificates) which the
+applicant can apply for, read also chapter on :ref:`Certificates
+<certificate>`.
+
+Applicant Interfaces
+====================
+
+As already mentioned, the applicant objects contains all information
+necessary for application, except for the payment ticket data. The
+base set of the applicant's 'external behaviour' is described by the
+following interface.
+
+`IApplicantBaseData`
+--------------------
+
+.. literalinclude:: ../../../../src/waeup/kofa/applicants/interfaces.py
+   :pyobject: IApplicantBaseData
+
+As usual, the interface lists attributes first. Except for the
+last two attributes (`password` and `application_date`), they are all
+read-only property attributes, i.e. attributes with a getter method
+only. These properties are computed dynamically and can't be set.
+
+`IApplicant`
+------------
+
+In the base package `IApplicant` is derived from `IApplicantBaseData`
+and only two methods are added:
+
+.. literalinclude:: ../../../../src/waeup/kofa/applicants/interfaces.py
+   :pyobject: IApplicant
+
+In custom packages we have furthermore interfaces for undergraduate
+and postgraduate students, both derived from `IApplicantBaseData`.
+
+Then there is a customized interface `ISpecialApplicant` for former
+students or students who are not users of the portal but have to pay
+supplementary fees. This reduced interface is used in browser
+components only, it does not instantiate applicant objects.
+
+Applicant Payment Interfaces
+============================
+
+`IApplicantOnlinePayment`
+-------------------------
+
+Instances of this interface are called applicant payment tickets.
+They contain the data which confirm that the applicant has paid the
+application fee.
+`waeup.kofa.students.interfaces.IStudentOnlinePayment` inherits from
+`waeup.kofa.payments.interfaces.IOnlinePayment` and promises three
+additional methods which process the applicant data after successful
+or approved payment.
+
+.. literalinclude:: ../../../../src/waeup/kofa/applicants/interfaces.py
+   :pyobject: IApplicantOnlinePayment
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/applicants/workflow.rst	(revision 16045)
@@ -0,0 +1,91 @@
+.. _application_workflow:
+
+Application Workflow
+====================
+
+For an introduction see :ref:`registration_workflow`. The already
+mentioned application workflow is shown here::
+
+            initialized
+             |
+             |a
+             |
+    +--+--- started ----------------------+
+    |  |     |                            |
+    |  |     |b                           |
+    |  |     |                            |
+    |  |h   paid   +-- processed          |
+    |  |     |     |                      |
+   i|  |     |c    |k                     |
+    |  |     |     |                      |
+    |  +--- submitted ---------+          |
+    |        |                 |          |
+    |        |d                |e         |
+    |        |         g       |          |
+    +------ admitted ----- not admitted --+
+             |
+             |f
+             |
+            created
+
+
+   a: start          -a: n/a
+   b: pay, approve   -b: reset1
+   c: submit         -c: reset2
+   d: admit          -d: n/a
+   e: refuse1        -e: n/a
+   f: create         -f: n/a
+   g: refuse2        -g: n/a
+   h: n/a            -h: reset3
+   i: n/a            -i: reset4
+   j: n/a            -j: reset5
+   k: process        -k: reset6
+
+Application starts with the creation of the applicant record, either
+by an anonymous user or by import. The first state is
+``initialized``. After first login, the state turns to ``started``
+**(a)**. Now the applicant is requested to fill the form, upload a
+passport picture and create a payment ticket. In contrast to student
+payments, making a payment and redeeming a payment is done in one
+step. Not only the ticket is marked ``paid``, but also the applicant
+is automatically set to state ``paid`` **(b)**. After successful
+payment the student can directly submit the application request
+**(c)**. Submitted records can be either sent back for editing and
+resubmission **(-c)**, accepted with admission confirmed **(d)** or
+accepted with admission refused **(e)**. Only applicant records
+with confirmed admission into the university can be transormed into
+student records. This final and **irreversible step** is accompanied
+by a transition to state ``created`` **(f)**.
+
+Submitted records can also be marked ``processed`` **(k)** in case
+the application module is used for other kinds of application
+processes which have nothing to do with student admission, e.g.
+transcript application or late payments by alumni.
+
+
+.. _application_history:
+
+Application History
+===================
+
+All transitions are automatically logged in ``applicants.log`` and the
+applicant's history. And also the import of workflow states is
+recorded in the logfile and the history, see :ref:`student_history`
+for further information. This is a sample history of an applicant
+which passes through the application process without any
+complications::
+
+  2015-06-23 08:56:23 UTC - Application initialized by Anonymous
+  2015-06-23 08:57:42 UTC - Application started by Demo Applicant
+  2015-06-23 08:59:41 UTC - Payment approved by Benny Goodman
+  2015-06-23 09:00:50 UTC - Application submitted by Demo Applicant
+  2015-06-23 09:01:13 UTC - Applicant admitted by Benny Goodman
+  2015-06-23 09:02:36 UTC - Student record created (K1000003) by Benny Goodman
+
+Benny Goodman is the name of an applications manager. If the
+workflow state is set by him by import, the following message would
+have been added instead::
+
+  2015-06-23 09:01:13 UTC - State 'admitted' set by Benny Goodman
+
+Applicant histories are exportable but cannot be imported.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/browsing.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/browsing.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/browsing.rst	(revision 16045)
@@ -0,0 +1,155 @@
+.. _browsing_kofa:
+
+
+Browsing Kofa
+*************
+
+The reader may ask: How do officers, applicants and students browse
+the portal? Where are the step-by-step operating instructions on how
+to use Kofa? We are always tempted to answer: The menu navigation
+should be self-explanatory and it's quite easy to follow the menu
+prompts or flash messages which appear above page titles in a green,
+yellow or red box. This answer seems a bit over the top. We must
+indeed describe some browser pages and what their clickable actions
+are doing with the data. The reason is, that a lot of Kofa's
+functionality is embedded in view and utility methods and not only
+provided by the functionality of content components. In the
+following, functionality of pages is described in subchapters called
+'Browser Pages'.
+
+
+.. _page_layout:
+
+Page Layout
+===========
+
+Kofa makes use of two different `Bootstrap`_ layouts. Anonymous
+users, students and applicants do see a single-column theme with
+only a static top navigation bar. Students can navigate to the
+various display form pages of their record by pulling down the 'My
+Data' tab in the navigation bar.
+
+.. _navigation_bar:
+
+The navigation bar for an anonymous user:
+
+.. image:: images/navbar_anon.png
+
+The navigation bar for a student with drop-down menue:
+
+.. image:: images/navbar_student.png
+
+The navigation bar for an officer with lots of permissions:
+
+.. image:: images/navbar_officer.png
+
+Officers see a double-column theme after logging in. The left column
+contains a box (side box) which provides links to the user's
+preferences (My Preferences) and roles (My Roles). It also contains
+links to the various sections of Kofa, depending on the permissions
+the officer has obtained. Possible sections are: 'Portal
+Configuration', 'Officers', 'Data Center', 'Reports' and 'Access
+Codes'. The side box expands, when the officer accesses a student
+record. The box gives direct access to the pages of the student.
+
+.. image:: images/multicolumn.png
+
+
+.. _views_pages:
+
+Views, Pages and Form Pages
+===========================
+
+Views are dealing with request and response objects. Usually a view
+renders (produces) HTML or PDF code to be displayed in a web browser
+or a pdf reader respectively. Very often a view only redirects to
+another view or page and does not render code by itself.
+Views, which render pdf code, are called pdf slips in Kofa.
+
+Kofa pages are 'layout-aware' browser views, which means they know
+about the global page layout and render content inside it.
+
+In Kofa most pages are form pages which means they are layout-aware
+views on data. These pages are either used to submit data (simple form
+page), or to display, edit or add persistant data. The latter three
+are called display, edit or add form pages respectively. Kofa is
+using the `Zope Formlib`_ package to auto-generate these forms.
+
+.. note::
+
+  Briefly, Zope Formlib is wedded to `Zope Schema`_, it provides
+  display and input widgets (= views) for the fields defined in the
+  Zope Schema package. Auto-generation is done with `grok.AutoFields`
+  which takes the fields, declared in an
+  :ref:`interface<kofa_interfaces>`, and renders display or input
+  widgets, according to the schema declaration, for display or edit
+  forms respectively.
+
+Whereas display and add form pages are usually shared by officers
+and students, edit form pages are not. Applicants and students are
+not allowed to edit all of their data all the time. Edit access is
+restricted by workflow states or other conditions. Officers' access
+is much less restricted, and we therefore speak of 'managing'
+instead of 'editing' data. In most cases, Kofa uses two different
+form pages which require different permissions: An `EditFormPage`
+requires the
+:py:class:`HandleApplication<waeup.kofa.applicants.permissions.HandleApplication>`
+/
+:py:class:`HandleStudent<waeup.kofa.students.permissions.HandleStudent>`
+permission and a `ManageFormPage` requires the
+:py:class:`ManageApplication<waeup.kofa.applicants.permissions.ManageApplication>`
+/
+:py:class:`ManageStudent<waeup.kofa.students.permissions.ManageStudent>`
+permission.
+
+
+.. _page_locking:
+
+Page Locking
+============
+
+As mentiond above, the right to use a form depends on the
+permissions the user gained. But this is not sufficient. Applicants
+and students always have the permission to handle their data
+although they are not allowed to edit the data all the time.
+Access to forms has to be further restricted. This is always done in
+the `update` method of a page. If an applicant or student doesn't
+meet the additional conditions defined in this method, s/he is
+immediately thrown out and redirected to another page. In most cases,
+the allowance to modify data depends on the workflow state of an
+applicant or student.
+
+
+.. _action_buttons:
+
+Action Buttons
+==============
+
+There are two kinds of action buttons which appear on pages:
+
+**Link Buttons** appear on top of the page above the page
+title and are decorated with an icon. These
+:py:class:`action
+buttons<waeup.kofa.browser.viewlets.ActionButton>` have a
+link target which means they usually refer to another Kofa
+page and are sending GET requests to open the page. Example:
+
+.. image:: images/link_button.png
+   :scale: 50 %
+
+**Form Buttons** are submit buttons which appear below a form.
+They are HTML form actions which submit data by sending a
+POST request back to the form page. A form page method is
+called and processes the data or simply redirects to
+another Kofa page. Example:
+
+.. image:: images/form_buttons.png
+   :scale: 50 %
+
+
+
+.. _bootstrap: http://getbootstrap.com/
+
+.. _zope schema: http://docs.zope.org/zope.schema
+
+.. _zope formlib: http://bluebream.zope.org/doc/1.0/manual/schema.html#auto-generated-forms-using-the-forms-package
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/buildingdocs.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/buildingdocs.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/buildingdocs.rst	(revision 16045)
@@ -0,0 +1,62 @@
+.. _building_documentation:
+
+Building Documentation (Linux OS only)
+**************************************
+
+We use the excellent `Sphinx <http://sphinx.pocoo.org/>`_ Python
+documentation generator to generate the docs as HTML pages.
+
+Prerequisites
+=============
+
+We assume you already installed `waeup.kofa` as explained in
+:ref:`installing_linux`. You ran ``bin/buildout`` and can find a script
+``bin/waeupdocs``.
+
+Building HTML docs
+==================
+
+The documentation of the :mod:`waeup.kofa` project can easily be
+created doing::
+
+  $ bin/waeupdocs
+
+This will create a tree of HTML pages in
+``docs/build/html`` which you can for
+instance browse by pointing your browser to this location.
+
+An 'official' place in internet for the whole docs is about to come
+but not yet available.
+
+Actually, ``waeupdocs`` is only a wrapper script around the real main
+script ``sphinx-build``, which should also be located in your ``bin/``
+directory.
+
+You therefore can also run ``bin/sphinx-build`` manually, which will
+give you more options, especially, if you want the output to be
+created in a different location or similar.
+
+Run::
+
+   $ bin/sphinx-build --help
+
+to get a list of options available.
+
+Alternatively, you can also go to ``docs/build`` and run::
+
+   $ make html
+
+Other available formats are listed, when you run only::
+
+   $ make
+
+These other formats will include ``latex``, ``doctest``, ``linkcheck``
+and more with a short explanation for each format.
+
+Building PDF docs
+=================
+
+We're currently not prepared for PDF, but support is already planned.
+
+Please be aware, that any PDF support will require a full TeX
+installation on your system to work.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/configuration.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/configuration.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/configuration.rst	(revision 16045)
@@ -0,0 +1,76 @@
+.. _configuration:
+
+Portal Configuration
+********************
+
+There are many places in Kofa where portal parameters can be set or
+page descriptions can be added. These parameters are attributes of
+either dedicated configuration objects or of containers in the
+academics, applicants or data center sections. The latter have been
+described elsewehere. Here we explain the interfaces of the
+configuration section.
+
+Technically speaking, the portal configuration section is a
+container of type `ConfigurationContainer` with id ``configuration``
+which contains session configuration subobjects::
+
+
+  Portal Configuration (ConfigurationContainer)
+  |
+  +--->SessionConfiguration
+
+Much like ``academics``, ``students``, ``applicants``, ``hostels``
+and ``documents``, also ``configuration`` is a unique container
+which is located in the `IUniversity` instance.
+
+Site Settings
+=============
+
+Site parameters are the attributes of the configuration container to
+be set via the `ConfigurationContainerManageFormPage`. The page
+opens, if an officer with
+:py:class:`ManagePortalConfiguration<waeup.kofa.permissions.ManagePortalConfiguration>`
+permission clicks the 'Portal Configuration' link in the side box.
+
+The `ConfigurationContainer` class implements exactly one interface:
+
+
+.. literalinclude:: ../../../src/waeup/kofa/interfaces.py
+   :pyobject: IConfigurationContainer
+
+The page furthermore lists containing session configuration objects
+and allows to add them.
+
+Session Settings
+================
+
+Session parameters are the attributes of session configuration
+objects. These parameters may vary between academic sessions.
+Particularly student fees, which often vary from one session to the
+next, can be configured here, if the portal has been customized
+accordingly, see :ref:`customization` below. The session
+configuration objects also serve to:
+
+- disable student payments for certain payment categories and
+  subgroups of students
+
+  See also `Disable Payment Groups
+  <https://kofa-demo.waeup.org/sources#collapseDisablePaymentGroups>`_
+  in the base package.
+
+- disable clearance by clearance officers for the selected session
+
+  If clearance is disabled, students can still fill and submit the
+  clearance form, but clearance officers can't process the clearance
+  requests afterwards. They can neither clear these students nor
+  reject their clearance request.
+
+- set the course registration deadline
+
+  If the course registration deadline is set, student can't register
+  courses unless they pay the late registration fee.
+
+Session configuration objects implement the following interface:
+
+.. literalinclude:: ../../../src/waeup/kofa/interfaces.py
+   :pyobject: ISessionConfiguration
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/customization.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/customization.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/customization.rst	(revision 16045)
@@ -0,0 +1,243 @@
+.. _customization:
+
+Portal Customization
+********************
+
+Portal customization requires profound knowledge of the Python
+programming language. Customization is part of the portal deployment
+process. It can only be done by the custom package manager and
+activated by the system administrator, who is allowed to
+update/upgrade the package on the server and restart the portal.
+Usually, custom packages are not designed for regular
+reconfiguration. Due to the complexity of school fee computation at
+Nigerian universities, however, a lot of configuration had to be
+done in the Python code and can thus not be subject to online
+configuration. Technically speaking, the parameters are only loaded
+and registered once during start-up and are not stored in the
+database like the configuration parameters described in
+:ref:`configuration`.
+
+Kofa must not be customized by manipulating original Kofa code, i.e.
+by editing Python files, Zope page templates or ZCML configuration
+files in the Kofa base package. Customization is done by deploying a
+custom package which can inherit all the Python classes from the
+base package, in order to override distinct attributes and methods
+and register these customized classes instead of the orginal classes
+during start-up. It would go beyond the scope of the user handbook
+to explain, how to build such a Kofa custom package. This will be
+done in the :ref:`developer_handbook`. Here we roughly describe the
+three different customization approaches with a focus on the third
+approach, the customization of utility attributes and methods.
+
+Localization
+============
+
+The base language of the Kofa User Interface (Kofa UI) is English.
+The English language regime has been specified by WAeUP's first
+customer, the University of Benin in Nigeria. Kofa is language-aware,
+which means it has been translated into different languages (German
+and French so far), and the preferred language can be selected via
+the UI. The Kofa UI translations and also the English language
+regime can be localized in custom packages by overriding single
+entries of the language files. Headlines, field labels, field
+descriptions, navigation tabs, buttons, page comments, flash
+messages etc. can be easily adjusted to the needs of the institution
+which wants to deploy Kofa.
+
+Kofa is using the `GNU gettext utilities
+<http://www.gnu.org/software/gettext>`_ for translation and
+localization. Translation is done via ``po`` text files which are
+located in the ``locales`` folder of a package. Each language has
+its own subfolder. Localized English Kofa terms, for instance, can be
+found in ``locales/en/LC_MESSAGES/waeup.kofa.po``. A single
+entry/message consists of two lines.
+
+Example::
+
+  msgid "Login"
+  msgstr "Let me in"
+
+``msgid`` contains the original term from the base package,
+``msgstr`` contains its localized/translated version, which results
+into a renamed login tab on the right side of the navigation bar.
+
+``po`` files must be compiled before restarting the portal. Editing
+and compilation of ``po`` files can be done with a nice tool called
+'Poedit' which is `available <https://poedit.net/download>`_ for
+various operating systems.
+
+
+Zope Interfaces
+===============
+
+Adding further schema field attributes to content components can be
+done by inheriting Zope interfaces from the base package and adding
+new schema fields to these custom interfaces, see :ref:`note
+<kofa_interfaces>`. This sounds simple, but it isn't. There are many
+things to think about, when customizing interfaces:
+
+1. A customized interface alone doesn't have any effect, beause the
+   content classes still implement the original interfaces. Thus, also
+   all content classes, which are supposed to implement the extended
+   interfaces, must be customized, and must be registered instead of
+   the orginal classes which they inherit from. Drawback is, that this
+   must be done before the first start-up of the portal. Otherwise,
+   instances of the original content classes might have been created
+   which cannot be changed afterwards.
+
+2. Also the forms which are being auto-generated by
+   `grok.AutoFields` (see :ref:`views_pages`), must be customized and
+   use the new and extended interfaces. Otherwise added fields won't
+   appear on form pages.
+
+3. The same holds for exporters and importers. They also consider
+   the fields from interfaces when processing the data. If we forget to
+   customize exporter or batch processor classes, only the fields from
+   the original interfaces will be exported or reimported.
+
+
+Zope Utilities
+==============
+
+Kofa's global utilities are perfectly suited for customization.
+Their attributes and methods can be easily overridden in custom
+packages.
+
+Kofa Utils
+----------
+
+The central `KofaUtils` utility contains not only most dictionaries
+used for sources (see :py:class:`API
+<waeup.kofa.utils.utils.KofaUtils>`) but also **configuration
+attributes** like:
+
+.. autoattribute:: waeup.kofa.utils.utils.KofaUtils.PORTAL_LANGUAGE
+   :noindex:
+
+.. autoattribute:: waeup.kofa.utils.utils.KofaUtils.tzinfo
+   :noindex:
+
+.. autoattribute:: waeup.kofa.utils.utils.KofaUtils.SYSTEM_MAX_LOAD
+   :noindex:
+
+Customizable **utility methods** are:
+
+.. automethod:: waeup.kofa.utils.utils.KofaUtils.sendContactForm
+   :noindex:
+
+.. automethod:: waeup.kofa.utils.utils.KofaUtils.fullname
+   :noindex:
+
+.. automethod:: waeup.kofa.utils.utils.KofaUtils.genPassword
+   :noindex:
+
+.. automethod:: waeup.kofa.utils.utils.KofaUtils.sendCredentials
+   :noindex:
+
+.. automethod:: waeup.kofa.utils.utils.KofaUtils.getPaymentItem
+   :noindex:
+
+Students Utilities
+------------------
+
+`StudentsUtils` contains the following **configuration attributes**:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.STUDENT_ID_PREFIX
+   :noindex:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.SKIP_UPLOAD_VIEWLETS
+   :noindex:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.PORTRAIT_CHANGE_STATES
+   :noindex:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.SEPARATORS_DICT
+   :noindex:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.STUDENT_EXPORTER_NAMES
+   :noindex:
+
+.. autoattribute:: waeup.kofa.students.utils.StudentsUtils.STUDENT_BACKUP_EXPORTER_NAMES
+   :noindex:
+
+Customizable **utility methods** are:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.getReturningData
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.setReturningData
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.setPaymentDetails
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.setBalanceDetails
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.increaseMatricInteger
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.constructMatricNumber
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.setMatricNumber
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.getAccommodationDetails
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.selectBed
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.getPDFCreator
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.renderPDF
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.renderPDFAdmissionLetter
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.renderPDFCourseticketsOverview
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.renderPDFTranscript
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.getDegreeClassNumber
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.warnCreditsOOR
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.getBedCoordinates
+   :noindex:
+
+.. automethod:: waeup.kofa.students.utils.StudentsUtils.clearance_disabled_message
+   :noindex:
+
+
+Applicants Utililties
+---------------------
+
+`ApplicantsUtils` contains the following **configuration attributes**:
+
+.. autoattribute:: waeup.kofa.applicants.utils.ApplicantsUtils.APP_TYPES_DICT
+   :noindex:
+
+.. autoattribute:: waeup.kofa.applicants.utils.ApplicantsUtils.SEPARATORS_DICT
+   :noindex:
+
+Customizable **utility methods** are:
+
+.. automethod:: waeup.kofa.applicants.utils.ApplicantsUtils.setPaymentDetails
+   :noindex:
+
+.. automethod:: waeup.kofa.applicants.utils.ApplicantsUtils.getApplicantsStatistics
+   :noindex:
+
+.. automethod:: waeup.kofa.applicants.utils.ApplicantsUtils.sortCertificates
+   :noindex:
+
+.. automethod:: waeup.kofa.applicants.utils.ApplicantsUtils.getCertTitle
+   :noindex:
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter.rst	(revision 16045)
@@ -0,0 +1,12 @@
+.. _datacenter:
+
+Data Center
+***********
+
+.. toctree::
+   :maxdepth: 3
+
+   datacenter/intro
+   datacenter/export
+   datacenter/import
+   datacenter/logging
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/export.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/export.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/export.rst	(revision 16045)
@@ -0,0 +1,385 @@
+.. _export:
+
+Data Export
+***********
+
+Regular data exporters (1) collect objects from specific containers,
+(2) iterate over the collected objects, (3) extract and mangle
+information from each object, (4) write the information of each object
+into a row of a CSV file and (5) finally provide the file for
+download. The CSV file is neither stored in the database nor archived
+in the filesystem. (3) and (4) means a flattening of the hierarchical
+data structure, i.e. a mapping of objects to flat relational data to
+be stored in a CSV table. The extracted information must not
+necessarily be based only on static attributes of the collected
+object. The data, finally stored in the CSV file, can also be derived
+from parent or child objects, or dynamically computed by the object's
+methods and property attributes. These methods and properties can
+retrieve information from everywhere in the portal's database. In the
+following we list all exporter classes including two attributes and a
+method description. The `fields` attribute contain the column titles
+of the export file. These are not necessarily only attributes of the
+exported objects.
+
+.. note::
+
+  The list of exported columns usually underlies heavy customizations.
+  In the Kofa base package only very few columns are being exported. In
+  some Kofa custom packages tons of data are being gathered from
+  applicants and students and the number of columns increases
+  accordingly.
+
+The `title` attribute unveils the name of the exporter under which
+this exporter will be displayed in the user interface. The
+`mangle_value()` method shows how some of fields are being dynamically
+computed.
+
+Regular Exporters
+=================
+
+User Exporter
+-------------
+
+.. autoclass:: waeup.kofa.userscontainer.UserExporter()
+
+  .. autoattribute:: waeup.kofa.userscontainer.UserExporter.fields
+  .. autoattribute:: waeup.kofa.userscontainer.UserExporter.title
+  .. automethod:: waeup.kofa.userscontainer.UserExporter.mangle_value()
+
+Faculty Exporter
+----------------
+
+.. autoclass:: waeup.kofa.university.export.FacultyExporter()
+
+  .. autoattribute:: waeup.kofa.university.export.FacultyExporter.fields
+  .. autoattribute:: waeup.kofa.university.export.FacultyExporter.title
+  .. automethod:: waeup.kofa.university.export.FacultyExporter.mangle_value()
+
+Department Exporter
+-------------------
+
+.. autoclass:: waeup.kofa.university.export.DepartmentExporter()
+
+  .. autoattribute:: waeup.kofa.university.export.DepartmentExporter.fields
+  .. autoattribute:: waeup.kofa.university.export.DepartmentExporter.title
+  .. automethod:: waeup.kofa.university.export.DepartmentExporter.mangle_value()
+
+Course Exporter
+---------------
+
+.. autoclass:: waeup.kofa.university.export.CourseExporter()
+
+  .. autoattribute:: waeup.kofa.university.export.CourseExporter.fields
+  .. autoattribute:: waeup.kofa.university.export.CourseExporter.title
+  .. automethod:: waeup.kofa.university.export.CourseExporter.mangle_value()
+
+Certificate Exporter
+--------------------
+
+.. autoclass:: waeup.kofa.university.export.CertificateExporter()
+
+  .. autoattribute:: waeup.kofa.university.export.CertificateExporter.fields
+  .. autoattribute:: waeup.kofa.university.export.CertificateExporter.title
+  .. automethod:: waeup.kofa.university.export.CertificateExporter.mangle_value()
+
+Certificate Course Exporter
+---------------------------
+
+.. autoclass:: waeup.kofa.university.export.CertificateCourseExporter()
+
+  .. autoattribute:: waeup.kofa.university.export.CertificateCourseExporter.fields
+  .. autoattribute:: waeup.kofa.university.export.CertificateCourseExporter.title
+  .. automethod:: waeup.kofa.university.export.CertificateCourseExporter.mangle_value()
+
+Access Code Batch Exporter
+--------------------------
+
+.. autoclass:: waeup.kofa.accesscodes.export.AccessCodeBatchExporter()
+
+  .. autoattribute:: waeup.kofa.accesscodes.export.AccessCodeBatchExporter.fields
+  .. autoattribute:: waeup.kofa.accesscodes.export.AccessCodeBatchExporter.title
+
+Access Code Exporter
+--------------------
+
+.. autoclass:: waeup.kofa.accesscodes.export.AccessCodeExporter()
+
+  .. autoattribute:: waeup.kofa.accesscodes.export.AccessCodeExporter.fields
+  .. autoattribute:: waeup.kofa.accesscodes.export.AccessCodeExporter.title
+  .. automethod:: waeup.kofa.accesscodes.export.AccessCodeExporter.mangle_value()
+
+Hostel Exporter
+---------------
+
+.. autoclass:: waeup.kofa.hostels.export.HostelExporter()
+
+  .. autoattribute:: waeup.kofa.hostels.export.HostelExporter.fields
+  .. autoattribute:: waeup.kofa.hostels.export.HostelExporter.title
+
+Bed Exporter
+------------
+
+.. autoclass:: waeup.kofa.hostels.export.BedExporter()
+
+  .. autoattribute:: waeup.kofa.hostels.export.BedExporter.fields
+  .. autoattribute:: waeup.kofa.hostels.export.BedExporter.title
+
+Document Exporter
+-----------------
+
+.. autoclass:: waeup.kofa.documents.export.DocumentExporterBase()
+
+  .. automethod:: waeup.kofa.documents.export.DocumentExporterBase.mangle_value()
+
+Application Data Exporters
+==========================
+
+Applicants Container Exporter
+-----------------------------
+
+.. autoclass:: waeup.kofa.applicants.export.ApplicantsContainerExporter()
+
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantsContainerExporter.fields
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantsContainerExporter.title
+
+Applicant Exporter
+------------------
+
+.. autoclass:: waeup.kofa.applicants.export.ApplicantExporter()
+
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantExporter.fields
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantExporter.title
+  .. automethod:: waeup.kofa.applicants.export.ApplicantExporter.mangle_value()
+
+Applicant Payment Exporter
+--------------------------
+
+.. autoclass:: waeup.kofa.applicants.export.ApplicantPaymentExporter()
+
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantPaymentExporter.fields
+  .. autoattribute:: waeup.kofa.applicants.export.ApplicantPaymentExporter.title
+  .. automethod:: waeup.kofa.applicants.export.ApplicantPaymentExporter.mangle_value()
+
+Student Data Exporters
+======================
+
+When starting a Student Data Exporter in the Data Center all student
+records will be taken into consideration, no matter what or where a
+student is studying. The exporter can also be started 'locally' at
+various levels in the academic section. Starting one of the exporters
+e.g. at faculty or department level means that only the data of
+students are exported who study in this faculty or department
+respectively. The exporter can also be started at certificate level.
+Then only the data of students, who are studying the named study
+course, will be taken into account. At course level the data of those
+students are being exported who have attended or taken this specific
+course.
+
+Student Data Exporter can be further configured through a
+configuration page. Search parameters like the student's current level,
+current session and current study mode can be set to filter sets of
+students in order to decrease the size of the export file. The set of
+filter parameters varies and depends on the 'locatation' from where
+the exporter is called. A completely different set of filter
+parameters is provided for courses. In this case the session and level
+can be selected when the course was taken by the student.
+
+Student Exporter
+----------------
+
+.. autoclass:: waeup.kofa.students.export.StudentExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.StudentExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.StudentExporter.title
+  .. automethod:: waeup.kofa.students.export.StudentExporter.mangle_value()
+
+Student Study Course Exporter
+-----------------------------
+
+.. autoclass:: waeup.kofa.students.export.StudentStudyCourseExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.StudentStudyCourseExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.StudentStudyCourseExporter.title
+  .. automethod:: waeup.kofa.students.export.StudentStudyCourseExporter.mangle_value()
+
+Student Study Level Exporter
+----------------------------
+
+.. autoclass:: waeup.kofa.students.export.StudentStudyLevelExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.StudentStudyLevelExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.StudentStudyLevelExporter.title
+  .. automethod:: waeup.kofa.students.export.StudentStudyLevelExporter.mangle_value()
+
+Course Ticket Exporter
+----------------------
+
+.. autoclass:: waeup.kofa.students.export.CourseTicketExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.CourseTicketExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.CourseTicketExporter.title
+  .. automethod:: waeup.kofa.students.export.CourseTicketExporter.mangle_value()
+
+Student Payment Exporter
+------------------------
+
+.. autoclass:: waeup.kofa.students.export.StudentPaymentExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.StudentPaymentExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.StudentPaymentExporter.title
+  .. automethod:: waeup.kofa.students.export.StudentPaymentExporter.mangle_value()
+
+Bed Ticket Exporter
+-------------------
+
+.. autoclass:: waeup.kofa.students.export.BedTicketExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.BedTicketExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.BedTicketExporter.title
+  .. automethod:: waeup.kofa.students.export.BedTicketExporter.mangle_value()
+
+.. note::
+
+  The above exporters refer to a specific content type (object class).
+  They export all attributes of these objects and a few additional
+  parameters derived from the parent objects. These exporters can be
+  used for reimport, or more precisely for backing up and restoring
+  data. The following 'special' exporters are made on request of some
+  universities to collect and compose student data for analysis and
+  postprocessing by the university.
+
+Outstanding Courses Exporter
+----------------------------
+
+.. autoclass:: waeup.kofa.students.export.OutstandingCoursesExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.OutstandingCoursesExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.OutstandingCoursesExporter.title
+  .. automethod:: waeup.kofa.students.export.OutstandingCoursesExporter.mangle_value()
+
+Data For Bursary Exporter
+-------------------------
+
+.. autoclass:: waeup.kofa.students.export.DataForBursaryExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.DataForBursaryExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.DataForBursaryExporter.title
+  .. automethod:: waeup.kofa.students.export.DataForBursaryExporter.mangle_value()
+
+Unpaid Payments Exporter
+------------------------
+
+.. autoclass:: waeup.kofa.students.export.UnpaidPaymentsExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.UnpaidPaymentsExporter.title
+
+Accommodation Payments Exporter
+-------------------------------
+
+.. autoclass:: waeup.kofa.students.export.AccommodationPaymentsExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.AccommodationPaymentsExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.AccommodationPaymentsExporter.title
+  .. automethod:: waeup.kofa.students.export.AccommodationPaymentsExporter.mangle_value()
+
+School Fee Payments Overview Exporter
+-------------------------------------
+
+.. autoclass:: waeup.kofa.students.export.SchoolFeePaymentsOverviewExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.SchoolFeePaymentsOverviewExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.SchoolFeePaymentsOverviewExporter.title
+  .. autoattribute:: waeup.kofa.students.export.SchoolFeePaymentsOverviewExporter.curr_year
+  .. automethod:: waeup.kofa.students.export.SchoolFeePaymentsOverviewExporter.mangle_value()
+
+Session Payments Overview Exporter
+----------------------------------
+
+.. autoclass:: waeup.kofa.students.export.SessionPaymentsOverviewExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.SessionPaymentsOverviewExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.SessionPaymentsOverviewExporter.title
+  .. automethod:: waeup.kofa.students.export.SessionPaymentsOverviewExporter.mangle_value()
+
+Study Levels Overview Exporter
+------------------------------
+
+.. autoclass:: waeup.kofa.students.export.StudyLevelsOverviewExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.StudyLevelsOverviewExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.StudyLevelsOverviewExporter.title
+  .. automethod:: waeup.kofa.students.export.StudyLevelsOverviewExporter.mangle_value()
+
+Combo Card Data Exporter
+------------------------
+
+.. autoclass:: waeup.kofa.students.export.ComboCardDataExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.ComboCardDataExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.ComboCardDataExporter.title
+  .. automethod:: waeup.kofa.students.export.ComboCardDataExporter.mangle_value()
+
+Data For Lecturer Exporter
+--------------------------
+
+.. autoclass:: waeup.kofa.students.export.DataForLecturerExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.DataForLecturerExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.DataForLecturerExporter.title
+  .. automethod:: waeup.kofa.students.export.DataForLecturerExporter.mangle_value()
+
+Transcript Data Exporter
+------------------------
+
+.. autoclass:: waeup.kofa.students.export.TranscriptDataExporter()
+
+  .. autoattribute:: waeup.kofa.students.export.TranscriptDataExporter.fields
+  .. autoattribute:: waeup.kofa.students.export.TranscriptDataExporter.title
+  .. automethod:: waeup.kofa.students.export.TranscriptDataExporter.mangle_value()
+
+File Export
+===========
+
+You want to export files (passport images, pdf slips), for instance
+of all students in a department or applicants in an applicants
+container? There is no facility in Kofa which does this job for you,
+but you can batch-download files by means of your operating system
+on your local machine. This works perfectly with the the `wget`
+command on Linux or MacOS computers. Windows does not provide such a
+command.
+
+If ``numbers.txt`` contains the application numbers of applicants in
+the applicants container ``xyz``, the following bash script will
+download all passport images directly onto you computer::
+
+  wget --save-cookies cookies.txt --keep-session-cookies --post-data 'form.login=my-username&form.password=my-password' https://my-kofa-site/login
+
+  for i in $(cat numbers.txt)
+  do
+   wget --load-cookies cookies.txt --output-document=$i.jpg https://my-kofa-site/applicants/xyz/$i/passport.jpg
+  done
+
+If ``numbers.txt`` contains the ids of students in a department, the
+following bash script will download all passport images directly
+onto you computer. The script is a slightly extended, more user
+friendly version of the script above::
+
+  if [ $# -lt 3 ]
+  then
+     echo "usage: $0 username password filename"
+     exit 1
+  fi
+
+  wget --save-cookies cookies.txt --keep-session-cookies --post-data "form.login=$1&form.password=$2" https://my-kofa-site/login
+
+  counter=0
+
+  while read variable
+  do
+     counter=$((counter+1))
+     wget --load-cookies cookies.txt --output-document $variable.jpg https://my-kofa-site/students/$variable/passport.jpg
+     if [[ "$counter" -gt 1000 ]]; then
+        wget --save-cookies cookies.txt --keep-session-cookies --post-data "form.login=$1&form.password=$2" https://my-kofa-site/login
+        counter=0
+     fi
+  done < $3
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import.rst	(revision 16045)
@@ -0,0 +1,11 @@
+.. _import:
+
+Data Import
+***********
+
+.. toctree::
+   :maxdepth: 3
+
+   import_stages
+   import_processors
+
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_processors.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_processors.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_processors.rst	(revision 16045)
@@ -0,0 +1,176 @@
+.. _import_processors:
+
+Batch Processors
+****************
+
+.. seealso::
+
+   :ref:`Batch Processing Doctests (The Caveman Story) <batchprocessing_txt>`
+
+   :ref:`Batch Processing via Browser Doctests <batchprocessing_browser_txt>`
+
+All batch processors inherit from the
+:py:class:`waeup.kofa.utils.batching.BatchProcessor` base class. The
+`doImport` method, described above, always remains unchanged. All
+processors have a property `available_fields` which defines the set
+of importable data. They correspond with the column titles of the
+import file. Available fields are usually composed of location fields,
+interface fields and additional fields. Overlaps are possible.
+Location fields define the minumum set of fields which are necessary
+to locate an existing object in order to update or remove it.
+Interface fields (schema fields) are the fields defined in the
+interface of the data entity. Additional fields are additionally
+needed for data processing. We further distinguish between required
+and optional fields or between schema and non-schema fields.
+
+In the following we list all available processors of the Kofa base
+package including some important methods which describe them best. We
+do not list available fields of each processor here. Available fields
+are shown in the browser user interface on the upload page of the
+portal. The processors of the Kofa base package can be viewed
+`here <http://kofa-demo.waeup.org/processors>`_.
+
+Regular Processors
+==================
+
+User Processor
+--------------
+
+.. autoclass:: waeup.kofa.authentication.UserProcessor()
+  :noindex:
+
+Faculty Processor
+-----------------
+
+.. autoclass:: waeup.kofa.university.batching.FacultyProcessor()
+  :noindex:
+
+Department Processor
+--------------------
+
+.. autoclass:: waeup.kofa.university.batching.DepartmentProcessor()
+  :noindex:
+
+Certificate Processor
+---------------------
+
+.. autoclass:: waeup.kofa.university.batching.CertificateProcessor()
+  :noindex:
+
+Course Processor
+----------------
+
+.. autoclass:: waeup.kofa.university.batching.CourseProcessor()
+  :noindex:
+
+Certificate Course Processor
+----------------------------
+
+.. autoclass:: waeup.kofa.university.batching.CertificateCourseProcessor()
+  :noindex:
+
+Access Code Batch Processor
+---------------------------
+
+.. autoclass:: waeup.kofa.accesscodes.batching.AccessCodeBatchProcessor()
+  :noindex:
+
+Access Code Processor
+---------------------
+
+.. autoclass:: waeup.kofa.accesscodes.batching.AccessCodeProcessor()
+  :noindex:
+
+Hostel Processor
+----------------
+
+.. autoclass:: waeup.kofa.hostels.batching.HostelProcessor()
+  :noindex:
+
+Bed Processor
+--------------
+
+.. autoclass:: waeup.kofa.hostels.batching.BedProcessor()
+  :noindex:
+
+Document Processor
+------------------
+
+.. autoclass:: waeup.kofa.documents.batching.DocumentProcessorBase()
+  :noindex:
+
+Application Data Processors
+===========================
+
+Applicants Container Processor
+------------------------------
+
+.. autoclass:: waeup.kofa.applicants.batching.ApplicantsContainerProcessor()
+  :noindex:
+
+Applicant Processor
+-------------------
+
+.. autoclass:: waeup.kofa.applicants.batching.ApplicantProcessor()
+  :noindex:
+
+Applicant Online Payment Processor
+----------------------------------
+
+.. autoclass:: waeup.kofa.applicants.batching.ApplicantOnlinePaymentProcessor()
+  :noindex:
+
+Student Data Processors
+=======================
+
+Student Processor
+-----------------
+
+.. autoclass:: waeup.kofa.students.batching.StudentProcessor()
+  :noindex:
+
+.. _student_study_course_processor:
+
+Student Study Course Processor
+------------------------------
+
+.. autoclass:: waeup.kofa.students.batching.StudentStudyCourseProcessor()
+  :noindex:
+
+Student Study Level Processor
+-----------------------------
+
+.. autoclass:: waeup.kofa.students.batching.StudentStudyLevelProcessor()
+  :noindex:
+
+.. note::
+
+  The student data processors described so far are mainly intended for
+  restoring data. If the portal is operated correctly and without
+  interruption and students follow the workflow from their first to the
+  final study year, there is no need to use the above batch processors
+  to maintain the data. The processors are not part of the student
+  registration management. The following processors can or sometimes
+  even must be integrated into the regular management of student data.
+  Scores have to be imported, new payment tickets have to created, the
+  verdicts have to be set or workflow transitions have to be triggered.
+
+.. _course_ticket_processor:
+
+Course Ticket Processor
+-----------------------
+
+.. autoclass:: waeup.kofa.students.batching.CourseTicketProcessor()
+  :noindex:
+
+Student Online Payment Processor
+--------------------------------
+
+.. autoclass:: waeup.kofa.students.batching.StudentOnlinePaymentProcessor()
+  :noindex:
+
+Verdict Processor
+-----------------
+
+.. autoclass:: waeup.kofa.students.batching.StudentVerdictProcessor()
+  :noindex:
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_stages.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_stages.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/import_stages.rst	(revision 16045)
@@ -0,0 +1,131 @@
+.. _import_stages:
+
+Stages of Batch Processing
+**************************
+
+.. seealso::
+
+   :ref:`Data Center Doctests <datacenter_txt>`
+
+The term 'data import' actually understates the range of functions
+importers really have. As already stated, many importers do not only
+restore data once backed up by exporters or, in other words, take
+values from CSV files and write them one-on-one into the database.
+The data undergo a complex staged data processing algorithm.
+Therefore, we prefer calling them 'batch processors' instead of
+importers. The stages of the import process are as follows.
+
+Stage 1: File Upload
+====================
+
+Users with permission
+:py:class:`waeup.manageDataCenter<waeup.kofa.permissions.ManageDataCenter>`
+are allowed to access the data center and also to use the upload
+page. On this page they can access an overview of all available
+batch processors. When clicking on a processor name, required,
+optional and non-schema fields show up in the modal window.
+Also a CSV file template, which can be filled and uploaded to avoid
+header errors, is being provided in this window.
+
+Many importer fields are of type 'Choice', which means only definied
+keywords (tokens) are allowed, see :ref:`schema fields <kofa_schemas>`.
+An overview of all sources and vocabularies, which feed the
+choices, can be also accessed from the datacenter upload page and
+shows up in a modal window. Sources and vocabularies of the base
+package can be viewed `here <http://kofa-demo.waeup.org/sources>`_.
+
+Data center managers can upload any kind of CSV file from their
+local computer. The uploader does not check the integrity of the
+content but the validity of its CSV encoding (see
+:py:func:`check_csv_charset<waeup.kofa.utils.helpers.check_csv_charset>`).
+It also checks the filename extension and allows only a limited
+number of files in the data center.
+
+.. autoattribute:: waeup.kofa.browser.pages.DatacenterUploadPage.max_files
+   :noindex:
+
+If the upload succeeded the uploader sends an email to all import
+managers (users with role
+:py:class:`waeup.ImportManager<waeup.kofa.permissions.ImportManager>`)
+of the portal that a new file was uploaded.
+
+The uploader changes the filename. An uploaded file ``foo.csv`` will
+be stored as ``foo_USERNAME.csv`` where username is the user id of
+the currently logged in user. Spaces in filename are replaced by
+underscores. Pending data filenames remain unchanged (see below).
+
+After file upload the data center manager can click the 'Process
+data' button to open the page where files can be selected for import
+(**import step 1**). After selecting a file the data center manager
+can preview the header and the first three records of the uploaded
+file (**import step 2**). If the preview fails or the header
+contains duplicate column titles, an error message is raised. The
+user cannot proceed but is requested to replace the uploaded file.
+If the preview succeeds the user is able to proceed to the next step
+(**import step 3**) by selecting the appropriate processor and an
+import mode. In import mode ``create`` new objects are added to the
+database, in ``update`` mode existing objects are modified and in
+``remove`` mode deleted.
+
+Stage 2: File Header Validation
+===============================
+
+Import step 3 is the stage where the file content is assessed for
+the first time and checked if the column titles correspond with the
+fields of the processor chosen. The page shows the header and the
+first record of the uploaded file. The page allows to change column
+titles or to ignore entire columns during import. It might have
+happened that one or more column titles are misspelled or that the
+person, who created the file, ignored the case-sensitivity of field
+names. Then the data import manager can easily fix this by selecting
+the correct title and click the 'Set headerfields' button. Setting
+the column titles is temporary, it does not modify the uploaded
+file. Consequently, it does not make sense to set new column titles
+if the file is not imported afterwards.
+
+The page also calls the `checkHeaders` method of the batch processor
+which checks for required fields. If a required column title is
+missing, a warning message is raised and the user can't proceed to
+the next step (**import step 4**).
+
+.. important::
+
+  Data center managers, who are only charged with uploading files but
+  not with the import of files, are requested to proceed up to import step 3
+  and verify that the data format meets all the import criteria and
+  requirements of the batch processor.
+
+Stage 3: Data Validation and Import
+===================================
+
+Import step 4 is the actual data import. The import is started by
+clicking the 'Perform import' button. This action requires the
+:py:class:`waeup.importData<waeup.kofa.permissions.ImportData>`
+permission. If data managers don't have this permission, they will
+be redirected to the login page.
+
+Kofa does not validate the data in advance. It tries to import the
+data row-by-row while reading the CSV file. The reason is that
+import files very often contain thousands or even tenthousands of
+records. It is not feasable for data managers to edit import files
+until they are error-free. Very often such an error is not really a
+mistake made by the person who compiled the file. Example: The
+import file contains course results although the student has not yet
+registered the courses. Then the import of this single record has to
+wait, i.e. it has to be marked pending, until the student has added
+the course ticket. Only then it can be edited by the batch processor.
+
+The core import method is:
+
+.. automethod:: waeup.kofa.utils.batching.BatchProcessor.doImport()
+   :noindex:
+
+Stage 4: Post-Processing
+========================
+
+The data import is finalized by calling
+:py:meth:`distProcessedFiles<waeup.kofa.datacenter.DataCenter.distProcessedFiles>`.
+This method moves the ``.pending`` and ``.finished`` files as well as the
+originally imported file from their temporary to their final location in the
+storage path of the filesystem from where they can be accessed through the
+browser user interface.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/intro.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/intro.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/intro.rst	(revision 16045)
@@ -0,0 +1,89 @@
+.. _datacenter_intro:
+
+Introduction
+************
+
+.. _object_database:
+
+The Object Database
+===================
+
+Most web portals store their data in a relational database like
+PostgreSQL, MySQL or Oracle. A relational database is organized in
+tables of rows and columns, with a unique key for each row. Each data
+entity gets its own table. Rows in tables can be linked to rows in
+other tables by storing the unique key of the row to which it should
+be linked. This sounds quite simple. Many computer users are familiar
+with this kind of data storage because they are used to spreadsheet
+programmes like Excel oder Calc which also organize data in tables.
+Kofa's persistent data are stored in a native object database designed
+for the Python programming language, the so-called ZODB_. An object
+database stores objects with attributes and not records as rows with
+columns in tables. These persistent objects can hold any kind of
+information in attributes and must not adhere to a specific schema
+like records in tables of a relational database.
+
+The ZODB_ also supports a hierarchical, treelike storage of objects.
+Objects can contain other objects if they are declared as containers.
+Objects are stored like folders and files in a filesystem. This makes
+the object handling very fast and transparent because we can access
+objects, or more precisely views of objects, by indicating their path
+in the database, i.e. by traversing the database tree to the object's
+location. Furthermore, we are accessing the views of objects through a
+web browser by entering a URL (Uniform Resource Locator). This
+publication path corresponds more or less to the traversal path of our
+objects. In Kofa the path always contains the object identifiers of
+all objects which are passed when traversing the database tree.
+Example:
+
+https://kofa-demo.waeup.org/students/K1000000/studycourse/100/DCO
+
+is the URL which requests a display view of a course ticket with id
+``DCO``. This object is stored in a study level container object with
+id ``100``, stored in a study course container object with id
+``studycourse``, stored in the student container object with id
+``K1000000``, stored in the students root container, stored in the
+root container of the application, stored in the root of the database
+itself.
+
+This kind of storage requires that each object gets a unique object
+identifier (object id) within its container. The id string is visible
+in the browser address bar. Though it's technically possible for ids
+to contain spaces or slashes we do not allow these kinds of special
+characters in object ids to facilitate the readability of URLs.
+
+Batch Processing
+================
+
+Administrators of web portals, which store their data in relational
+databases, are used to getting direct access to the portal's database.
+There are even tools to handle the administration of these databases
+over the Internet, like phpMyAdmin or phpPgAdmin to handle MySQL or
+PostgreSQL databases respectively. These user interfaces bypass the
+portals' user interfaces and give direct access to the database. They
+allow to easily import or export (dump) data tables or the entire
+database structure into CSV or SQL files. What at first sight appears
+to be very helpful and administration-friendly proves to be very
+dangerous on closer inspection. Data structures can be easily damaged
+or destroyed, or data can be easily manipulated by circumventing the
+portal's security machinery or logging system. Kofa does not provide
+any external user interface to access the ZODB_ directly, neither for
+viewing nor for editing data. This includes also the export and import
+of sets of data. Exports and imports are handled via the Kofa user
+interface itself. This is called batch processing which means either
+producing CSV files (comma-separated values) from portal data (export)
+or processing CSV files in order to add, update or remove portal data
+(import). Main premise of Kofa's batch processing technology is that
+the data stored in the ZODB_ can be specifically backed up and
+restored by exporting and importing data. But that's not all. Batch
+processors can do much more. They are an integral part of the student
+registration management.
+
+.. note::
+
+  Although exporters are part of Kofa's batch processing module, we will
+  not call them batch processors. Only importers are called batch
+  processors. Exporters produce CSV files, importers process them.
+
+
+.. _ZODB: http://www.zodb.org/
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/logging.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/logging.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/datacenter/logging.rst	(revision 16045)
@@ -0,0 +1,326 @@
+.. _logging:
+
+Logfiles
+********
+
+Kofa logs actions, which cause changes to the database or the
+filesystem, in separate human-redable event logfiles. Nearly all
+actions are being logged, except some actions committed by
+applicants and students during registration periods. During these
+data initialization periods, applicants and students are requested
+to edit many fields. Usually they press the 'Save' button quite
+often to backup the data entered so far. Logging all these changes
+would mean filling the logfiles with more or less useless or
+redundant information.
+
+The main purpose of Kofa's logging facilities is to trace changes
+effected by portal officers. Applicants and students can only access
+their own data. They don't have the permission to change anything
+else. Whereas portal officers bear responsibility for the integrity
+of the whole database. Depending on the roles they have, officers
+can add, edit or remove application and study data without leaving a
+trace in the object database itself. Kofa closes this security gap
+by recording nearly all actions of portal officers into various
+event logfiles.
+
+.. note::
+
+  Kofa logs the changing but not the viewing of data (except
+  downloading export files or reports). Also entering (login) end
+  leaving (logout) the portal is **not** being recorded.
+
+Critics may claim that extensive logging may discourage officers
+from working with the portal. Persons in key functions often don't
+want to be controlled or supervised by anybody else. Indeed, Kofa
+does not respond to the desire for this kind of privacy. Datacenter
+managers can search all logfiles and can even see what their
+principles have done with the data. Goal behind is **transparency
+and security**, i.e. transparent data processing and reduction of
+the vulnerability to corruption combined with cutting-edge
+technologies to protect the data against attacks from outside.
+
+Logfiles can not be accessed directly. They can neither be
+downloaded nor viewed entirely online. But the files can be searched
+online for arbitrary `regular expressions`_. The simplest regular
+expression is a single word or a string. A technical description,
+how logfiles are being querried, can be found
+:py:meth:`here<waeup.kofa.datacenter.DataCenter.queryLogfiles>`.
+
+Each line in the logfile means a single log entry. It is composed of
+four parts: a datetime string, the log level (mainly 'INFO'), the id
+of the logged-in user, and a message part. Depending on the module,
+the message part is subdivided into further parts which are
+described below.
+
+
+main.log
+========
+
+All actions in the academic section are logged in ``main.log``.
+Changes of faculties, departments, certificates, courses and
+certificate courses done via pages in the user interface (browser
+page) are recorded in the following form::
+
+  2014-12-11 13:09:22,855 - INFO - admin - browser.pages.DepartmentAddFormPage - added: FAC1/DEP1
+  2014-12-11 13:19:31,755 - INFO - admin - browser.pages.DepartmentManageFormPage - DEP1 - saved: title
+  2014-12-12 13:19:43,255 - INFO - admin - browser.pages.FacultyManageFormPage - removed: DEP1
+
+The message part is composed of the browser page name and the action
+taken. In the example above a department ``DEP1`` was added on
+December 11, 2014 by user ``admin`` in faculty ``FAC1``, the title
+was changed 10 minutes later and, finally, the department was
+entirely removed one day later. In the same way user data and
+configuration data are logged in the main logfile. Examples::
+
+  2015-04-01 08:33:47,474 - INFO - admin - browser.pages.UserManageFormPage - Test edited: roles
+  2015-04-27 09:39:24,073 - INFO - admin - browser.pages.SessionConfigurationAddFormPage - added: 2014
+
+Furthermore, ``main.log`` does contain information about reports::
+
+  2015-04-28 15:34:31,852 - INFO - admin - students.reports.student_statistics.StudentStatisticsReportGeneratorPage - report 3029 created: Student Statistics (session=1989, mode=All, breakdown=faccode)
+  2015-04-28 15:34:37,698 - INFO - admin - students.reports.student_statistics.StudentStatisticsReportPDFView - report 3029 downloaded: StudentStatisticsReport_1989_1990_All_2015-04-28_13-34-36_UTC.pdf
+  2015-04-28 15:36:30,106 - INFO - admin - browser.reports.ReportsContainerPage - report 3029 discarded
+
+the management of documents::
+
+  2015-01-09 17:30:49,819 - INFO - admin - HOWTO - Document created
+  2015-01-09 17:30:49,822 - INFO - admin - documents.browser.DocumentAddFormPage - added: REST Document HOWTO
+
+password change requests and the usage of password mandates::
+
+  2015-04-28 13:42:52,753 - INFO - zope.anybody - browser.pages.ChangePasswordRequestPage - B1234567 - myname@gmail.com
+  2015-04-28 13:43:51,056 - INFO - zope.anybody - PasswordMandate used: B1234567
+
+``zope.anybody`` is the user id of anonnymous (non-logged-in) users.
+
+Some log entries do not contain information about a browser page.
+These entries were generated deeper in the system. The ``Document
+created`` entry, for example, was added by a workflow transition.
+The ``PasswordMandate used`` entry was added by
+:py:meth:`waeup.kofa.mandates.mandate.PasswordMandate.execute` and
+not directly by the browser page which calls this method
+(:py:meth:`waeup.kofa.mandates.browser.MandateView.update`).
+
+``main.log`` is also the place where plugins store information about
+system upgrades. If catalogs have to be re-indexed or new attributes
+of objects have to be initialized, a corresponding message is stored
+in the main logfile.
+
+
+datacenter.log
+==============
+
+All actions in the data center (uploading, processing, downloading
+and deleting of files) are logged in ``datacenter.log``::
+
+  2015-04-29 06:52:23,483 - INFO - admin - browser.pages.DatacenterUploadPage - uploaded: /kofa/trunk/var/datacenter/users_admin.csv
+  2015-04-29 06:52:44,025 - INFO - admin - processed: /kofa/trunk/var/datacenter/users_admin.csv, update mode, 3 lines (0 successful/ 3 failed), 0.129 s (0.0430 s/item)
+  2015-04-29 06:53:10,404 - INFO - admin - browser.pages.DatacenterPage - deleted: users_admin.update.pending.csv
+  2015-04-29 06:54:41,963 - INFO - admin - browser.pages.DatacenterUploadPage - uploaded: /kofa/trunk/var/datacenter/users2_admin.csv
+  2015-04-29 06:54:51,748 - INFO - admin - processed: /kofa/trunk/var/datacenter/users2_admin.csv, create mode, 3 lines (3 successful/ 0 failed), 0.024 s (0.0079 s/item)
+
+In the example above a user data file ``users.csv`` was uploaded and
+processed in update mode. The import failed since the 3 users didn't
+exist. The file was then deleted. User ``admin`` uploaded the file a
+second time. The file name had to be changed because the uploader
+does not allow to upload files with same name twice. The file was
+finally successfully processed in create mode.
+
+Also the export of data (export, download and discard) is recorded
+in detail::
+
+  2015-04-29 06:55:20,485 - INFO - admin - browser.pages.ExportCSVPage - exported: certificates, job_id=3036
+  2015-04-29 06:55:25,697 - INFO - admin - browser.pages.ExportCSVView - downloaded: WAeUP.Kofa_certificates_3036.csv, job_id=3036
+  2015-04-29 06:55:30,579 - INFO - admin - browser.pages.ExportCSVPage - discarded: job_id=3036
+
+
+accesscodes.log
+===============
+
+The creation of access code batches, disabling and re-enabling of
+single access codes and archiving and removal of entire batches is
+logged as follows::
+
+  2015-04-29 08:12:26,091 - INFO - admin - accesscodes.browser.AddBatchPage - created: ABC-1-2015_04_29_06_12_24-admin.csv (1000, 2300.000000)
+  2015-04-29 08:13:07,024 - INFO - admin - accesscodes.browser.BatchContainerSearchPage - disabled: ABC-1-6003657048
+  2015-04-29 08:13:11,502 - INFO - admin - accesscodes.browser.BatchContainerSearchPage - (re-)enabled: ABC-1-6003657048
+  2015-04-29 08:14:13,668 - INFO - admin - accesscodes.browser.BatchContainerPage - archived: ABC-1 (ABC-1_archive-2015_04_29_06_14_13-admin.csv)
+  2015-04-29 08:14:14,152 - INFO - admin - accesscodes.browser.BatchContainerPage - deleted: ABC-1
+
+Not all access code workflow transitions are being logged. Each
+access code has a history attribute which contains a detailed list
+of all transitions. The history is shown on the
+`BatchContainerSearchPage`.
+
+
+applicants.log
+==============
+
+The creation, editing and removal of applicants containers as well
+as editing applicant records is being logged. Also
+the approval of payment tickets and all other payment ticket
+transactions are being recorded in ``applicants.log``. Kofa also
+logs all workflow transitions into both the applicant's history
+attribute and the logfile. Okay, this is somehow redundant, but it
+has proved useful to get a complete overview over all applicant
+data transactions also in the logfile. In return, Kofa does not
+aditionally log actions of browser pages if a workflow transition is
+triggered at the same time. Let's see the example::
+
+  2015-04-29 10:14:40,565 - INFO - admin - applicants.browser.ApplicantsContainerAddFormPage - added: app2015
+  2015-04-29 10:14:45,398 - INFO - admin - applicants.browser.ApplicantsContainerManageFormPage - app2015 - saved: startdate + enddate + application_fee
+  2015-04-29 10:15:08,779 - INFO - admin - app2015_262037 - Application initialized
+  2015-04-29 10:15:28,703 - INFO - admin - app2015_262037 - Application started
+  2015-04-29 10:15:28,704 - INFO - admin - applicants.browser.ApplicantManageFormPage - app2015_262037 - saved: reg_number + sex + course1 + date_of_birth
+  2015-04-29 10:16:27,654 - INFO - admin - applicants.browser.ApplicationFeePaymentAddPage - app2015_262037 - added: p4302953958139
+  2015-04-29 10:16:38,921 - INFO - admin - app2015_262037 - Payment approved
+  2015-04-29 10:16:38,922 - INFO - admin - applicants.browser.OnlinePaymentApprovePage - app2015_262037 - approved: p4302953958139
+  2015-04-29 10:16:58,026 - INFO - admin - app2015_262037 - Application submitted
+  2015-04-29 10:17:01,040 - INFO - admin - applicants.browser.ApplicantManageFormPage - app2015_262037 - saved: course_admitted
+  2015-04-29 10:17:10,978 - INFO - admin - app2015_262037 - Applicant admitted
+  2015-04-29 10:17:10,979 - INFO - admin - applicants.browser.ApplicantManageFormPage - app2015_262037 - saved: locked
+  2015-04-29 10:17:34,135 - INFO - admin - app2015_262037 - Student record created (K1000004)
+  2015-04-29 10:45:15,298 - INFO - admin - app2015_262037 - Applicant record removed
+  2015-04-29 10:45:15,299 - INFO - admin - applicants.browser.ApplicantsRootManageFormPage - removed: app2015
+
+An applicants container was added first. The `startdate`, `enddate`
+and the `application_fee` attributes were edited and a new
+applicant record was added some seconds later. The
+`ApplicantManageFormPage` was opened, `reg_number`, `sex`, `course1`
+and `date_of_birth` was edited and the ``start`` transition was
+selected. This was done in the same transaction. The time difference
+between both log entries is only 0.001s. Furthermore, a payment
+ticket was created and the payment approved. Then the applicant was
+set to ``sumitted``, a `course_admitted` was selected and the
+applicant subsequently admitted. The form was automatically locked
+(see time difference). A student container was created and filled
+with the data from the applicant record. Finally, the entire
+applicants container including its content was removed in the same
+transaction, see time diffence between the last two log entries.
+
+
+students.log
+============
+
+The following example shows a typical Nigerian logfile excerpt for a
+student from the very beginning (student record creation from
+applicant data) till the first registration of courses at level
+100. Such an excerpt can be produced simply by searching for
+``B1234567`` on the ``students.log`` search page::
+
+  2014-12-11 10:21:21,930 - INFO - admin - B1234567 - Record created
+  2014-12-11 10:21:21,935 - INFO - admin - B1234567 - Admitted
+  2014-12-19 05:53:02,760 - INFO - admin - Student Processor - physical_clearance_date_adm - B1234567 - updated: reg_number=12345678AB, physical_clearance_date=FRIDAY 9TH JANUARY 2015
+  2014-12-22 14:16:50,494 - INFO - B1234567 - waeup.kofa.students.browser.OnlinePaymentAddFormPage - B1234567 - added: p4192542104720
+  2014-12-22 14:51:36,959 - INFO - B1234567 - waeup.kofa.interswitch.browser.InterswitchPaymentRequestWebservicePageStudent - B1234567 - valid callback for clearance payment p4192542104720: 00:Approved Successful:4500000:5964:p4192542104720:FBN|WEB|UNIBEN|22-12-2014|02345:000382768769
+  2014-12-22 14:51:36,985 - INFO - B1234567 - waeup.kofa.interswitch.browser.InterswitchPaymentRequestWebservicePageStudent - B1234567 - successful clearance payment: p4192542104720
+  2014-12-22 14:53:09,148 - INFO - B1234567 - B1234567 - Clearance started
+  2014-12-22 14:53:39,983 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: birth_certificate.jpg (birth.JPG)
+  2014-12-22 14:53:54,482 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: acc_let.jpg (acceptance.JPG)
+  2014-12-22 14:54:08,943 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: lga_ident.jpg (LGA.JPG)
+  2014-12-22 14:54:23,541 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: fst_sit_scan.jpg (waec.JPG)
+  2014-12-22 14:54:56,663 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: ref_let.jpg (guarantor.JPG)
+  2014-12-22 14:56:16,039 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: stat_dec.jpg (good.JPG)
+  2014-12-22 14:56:37,895 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: jamb_letter.jpg (JAMB.JPG)
+  2014-12-22 14:56:54,696 - INFO - B1234567 - waeup.kofa.students.browser.StudentClearanceEditFormPage - B1234567 - uploaded: secr_cults.jpg (cult.JPG)
+  2014-12-22 15:02:42,550 - INFO - B1234567 - B1234567 - Clearance requested
+  2015-01-17 11:58:12,643 - INFO - clearanceofficer - B1234567 - Cleared
+  2015-03-10 10:58:46,217 - INFO - B1234567 - waeup.kofa.students.browser.OnlinePaymentAddFormPage - B1234567 - added: p4259815262042
+  2015-03-13 11:38:21,658 - INFO - B1234567 - waeup.kofa.interswitch.browser.InterswitchPaymentRequestWebservicePageStudent - B1234567 - valid callback for schoolfee payment p4259815262042: 00:Approved Successful:4950000:2022:p4259815262042:GTB|WEB|UNIBEN|13-03-2015|001234:0001234566
+  2015-03-13 11:38:22,016 - INFO - B1234567 - waeup.kofa.interswitch.browser.InterswitchPaymentRequestWebservicePageStudent - B1234567 - successful schoolfee payment: p4259815262042
+  2015-03-16 13:01:09,989 - INFO - B1234567 - B1234567 - First school fee payment made
+  2015-03-16 13:03:50,977 - INFO - B1234567 - waeup.kofa.students.browser.StudyLevelEditFormPage - B1234567 - added: PHY124|100|2014
+  2015-03-16 13:05:04,504 - INFO - B1234567 - B1234567 - Courses registered
+  2015-03-28 07:18:21,846 - INFO - admin - Student Processor - Matno_for_upload_25_03_2015-1_admin - B1234567 - updated: student_id=B1234567, matric_number=LSC123456
+
+The log entry format of transactions due to imports is slightly
+different::
+
+  2015-04-30 08:40:51,088 - INFO - admin - Student Processor - studentcreate_admin - B3333333 - updated: state=returning, reg_number=BIA12326, firstname=FRIDAY, middlename=None, lastname=Olonko, sex=m, nationality=NG, matric_number=SSC4444444
+  2015-04-30 08:41:27,179 - INFO - admin - StudentStudyCourse Processor (update only) - studycourseaupdate_admin - B3333333 - updated: entry_mode=ug_pt, certificate=BIA, current_session=2002, entry_session=2002, current_level=100, current_verdict=Z
+
+Two files had been uploaded by admin (``studentcreate.csv`` and
+``studycourseaupdate.csv``, see entries in ``datacenter.log``) and
+subsequently imported with the Student Processor and Student Study
+Course Processor respectively.
+
+As already stated above, not all actions committed by students are
+beeing logged. The student did not only upload a lot of files but
+also edited dozens of fields on the clearance form page and probably
+pressed the 'Save' button very often to backup the data entered so
+far. These backup transactions were skipped. All other transactions
+are being recorded. Some examples of further logfile records are
+listed below.
+
+Setting a temporary password::
+
+  2012-10-20 20:59:28,392 - INFO - admin - students.browser.LoginAsStudentStep1 - W4444444 - temp_password generated: Paa3ZVrV
+
+Deactivating and re-activating students::
+
+  2014-11-15 14:28:49,859 - INFO - admin - students.browser.StudentDeactivatePage - W1111111 - account deactivated
+  2014-11-17 10:19:05,735 - INFO - admin - students.browser.StudentActivatePage - W1111111 - account activated
+
+Rejection of clearance requests (an email is sent and the body of
+the email is stored as comment in the logfile)::
+
+  2014-10-27 12:39:34,499 - INFO - clearanceofficer - students.browser.StudentRejectClearancePage - W2222222 - comment: no credit in english
+
+Approval of payment tickets (which does not automatically goes along
+with a student workflow transition)::
+
+  2012-11-02 12:01:03,022 - INFO - admin - students.browser.OnlinePaymentApprovePage - W1000000 - schoolfee payment approved: p3517158705027
+
+Bed allocation and re-allocation::
+
+  2012-11-07 12:56:26,745 - INFO - W1000000 - waeup.kwarapoly.students.browser.BedTicketAddPage - W1000000 - booked: white-house_W_106_B
+  2013-10-29 11:49:55,048 - INFO - hostelofficer - students.browser.BedTicketRelocationPage - W1000000 - relocated: block-a-upper-hostel_A_118_E
+
+Password changes by students::
+
+  2013-02-19 08:54:50,177 - INFO - K9999999 - students.browser.StudentChangePasswordPage - K9999999 - saved: password
+
+
+payments.log
+============
+
+This file is hidden and can only be accessed directly in the
+filesystem. The payment logger is not needed in the Kofa base
+package. Only `OnlinePaymentApprovePage.update()` writes into the
+``payments.log`` file. The logger is primarily intended for storing
+information about successfull financial transactions via external
+payment gateways (e.g. PayPal, Interswitch, eTranzact). No external
+payment gateway module is configured in the base package.
+
+
+hostels.log
+===========
+
+Whereas the reservation/allocation of bed spaces is being logged in
+``students.log`` (see above), the management of hostels and beds is
+recorded in ``hostels.log``::
+
+  2015-04-30 11:44:29,145 - INFO - admin - hostels.browser.HostelAddFormPage - hostels - added: Hall 1
+  2015-04-30 11:45:29,283 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - saved: rooms_per_floor + blocks_for_female
+  2015-04-30 11:46:29,330 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - 0 empty beds removed, 10 beds added, 0 occupied beds modified ()
+  2015-04-30 11:47:29,433 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - switched: hall-1_A_101_A (reserved), hall-1_A_101_B (reserved), hall-1_A_101_C (reserved), hall-1_A_101_D (reserved)
+  2015-04-30 11:47:59,283 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - saved: beds_for_fresh + beds_for_all
+  2015-04-30 11:48:29,498 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - 9 empty beds removed, 9 beds added, 1 occupied beds modified (hall-1_A_101_E, )
+  2015-04-30 11:49:29,560 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - switched: hall-1_A_101_A (unreserved), hall-1_A_101_B (unreserved), hall-1_A_101_C (unreserved), hall-1_A_101_D (unreserved)
+  2015-04-30 11:50:29,689 - INFO - admin - hostels.browser.HostelManageFormPage - hall-1 - released: hall-1_A_101_D (K1000000)
+  2015-04-30 11:51:29,898 - INFO - admin - hostels.browser.BedManageFormPage - hall-1_A_101_A - saved: owner
+  2015-04-30 11:54:30,163 - INFO - admin - hostels.browser.HostelsContainerManagePage - hostels - deleted: hall-1
+
+In this example, the hostel ``Hall 1`` was added and configured,
+beds were updated, beds were reserved (switched), the hostel
+configuration was changed, beds were updated again, beds were
+unreserved, a single bed was released, a new student was allocated
+and, finally, the entire hostel was removed.
+
+Also all transactions of the Hostel Processor are being logged::
+
+  2015-04-30 11:51:12,840 - INFO - system - Hostel Processor - sample_hostel_data - hall-b - updated: beds_for_pre=['F'], floors_per_block=1, beds_for_final=['A', 'B'], rooms_per_floor=44, blocks_for_male=['C', 'D'], hostel_id=hall-b, sort_id=100, beds_for_returning=['C'], hostel_name=Hall B, beds_for_fresh=['D', 'E'], blocks_for_female=[], beds_for_all=[], beds_reserved=[]
+
+
+
+.. _regular expressions: http://en.wikipedia.org/wiki/Regular_expression
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/documents.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/documents.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/documents.rst	(revision 16045)
@@ -0,0 +1,135 @@
+.. _documents:
+
+Documents Section
+*****************
+
+The documents section of Kofa is a tiny built-in content management
+system (CMS), which has been developed mainly for publishing
+announcements, adverts, instructions or general information provided
+by the university to inform prospective students about application
+and registration procedures.
+
+Technically speaking, the documents section is a container of type `DocumentsContainer` with id ``documents``, which is located in the `IUniversity` instance and which contains public documents. There are three types of public documents::
+
+  Documents Section (DocumentsContainer)
+  |
+  +---> PDFDocument
+  |
+  +---> HTMLDocument
+  |
+  +---> RESTDocument
+
+The first can be used to provide pdf files for download. The second
+and third can be used to create multi-lingual static html pages on
+the portal. HTML documents expect html coded text as input, REST
+documents expect `reStructuredText <http://docutils.sf.net/rst.html>`_
+which is transformed into html.
+
+Interfaces
+==========
+
+The base for all three document interfaces is `IPublicDocument`
+which inherits from `IDocument`. All attributes are read-only
+properties, i.e. attributes with a getter method only. These
+properties are computed dynamically and can't be set. Only the
+`document_id` and the `title` can be entered on form pages or can be
+imported.
+
+.. literalinclude:: ../../../src/waeup/kofa/documents/interfaces.py
+   :pyobject: IDocument
+
+.. literalinclude:: ../../../src/waeup/kofa/documents/interfaces.py
+   :pyobject: IPublicDocument
+
+A PDF Document further specifies which pdf files are connected to
+the object. Usually, only one filename is in the `filenames` tuple.
+
+.. literalinclude:: ../../../src/waeup/kofa/documents/interfaces.py
+   :pyobject: IPDFDocument
+
+HTML and REST documents have a schema field for multi-lingual
+content in HTML or in REST format respectively, see :ref:`note
+<multilingual>`. The hidden `html_dict` attributes contain the same
+information after saving the form, but the sequence of language
+translations has been split up and copied into a dictionary for
+faster processing.
+
+.. literalinclude:: ../../../src/waeup/kofa/documents/interfaces.py
+   :pyobject: IHTMLDocument
+
+.. literalinclude:: ../../../src/waeup/kofa/documents/interfaces.py
+   :pyobject: IRESTDocument
+
+
+Workflow
+========
+
+Public documents have a publication workflow with two states:
+created and published. Only the content or attached files of
+published documents can be seen by anonymous users::
+
+                 a
+    created ----------- published
+
+    a: publish
+   -a: retract
+
+History & Logging
+=================
+
+All transitions are automatically logged in ``main.log`` and the
+document's history. This is a sample history of public document::
+
+  2015-07-05 07:31:32 UTC - Document created by Admin
+  2015-07-05 07:31:53 UTC - Document published by Admin
+  2015-07-05 07:31:57 UTC - Document retracted by Admin
+
+The corresponding logfile excerpt is as follows::
+
+  2015-07-05 09:31:32,848 - INFO - admin - demodoc - Document created
+  2015-07-05 09:31:32,850 - INFO - admin - documents.browser.DocumentAddFormPage - added: HTML Document demodoc
+  2015-07-05 09:31:39,025 - INFO - admin - documents.browser.HTMLDocumentManageFormPage - demodoc - saved: html_multilingual
+  2015-07-05 09:31:43,279 - INFO - admin - documents.browser.HTMLDocumentManageFormPage - added: benny|waeup.local.DocumentManager
+  2015-07-05 09:31:53,152 - INFO - admin - demodoc - Document published
+  2015-07-05 09:31:57,216 - INFO - admin - demodoc - Document retracted
+  2015-07-05 09:32:46,679 - INFO - admin - documents.browser.DocumentsContainerManageFormPage - removed: demodoc
+
+
+Quick Guide
+===========
+
+How to add and reference an HTML document
+-----------------------------------------
+
+1. Click *Documents > Manage > Add document*.
+2. Select *HTML Document*, fill fields and press *Add document*.
+3. Enter multi-lingual content in HTML format and press *Save*.
+4. Click *View > Transition*.
+5. Select *Publish document* and press *Save*.
+
+The document is now published and available to anonymous users.
+
+6. Mark and copy the HTML Element provided on the page:
+   ``<a href= ... /a>``.
+7. Open another HTML document or a page with multi-lingual content
+   and paste the element where needed.
+
+
+How to add and reference a PDF document
+---------------------------------------
+
+1. Click *Documents > Manage > Add document*.
+2. Select *PDF Document*, fill fields and press *Add document*.
+3. Select *Files* and upload a pdf file from your computer.
+4. Click *View > Transition*.
+5. Select *Publish document* and press *Save*.
+
+The document and the connected pdf file are now published and
+available to anonymous users.
+
+6. Open the HTML source with your browser, search for the ``a``
+   element
+7. Mark and copy the HTML element found:
+   ``<a target="image" href= ... /a>``.
+8. Open another HTML document or a page with multi-lingual content
+   and paste the element where needed.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/hostels.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/hostels.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/hostels.rst	(revision 16045)
@@ -0,0 +1,232 @@
+.. _accommodation_section:
+
+Accommodation Section
+*********************
+
+The accommodation section is a built-in hostel management system.
+African universities usually run their own student hostels which are
+built on their huge university campuses. They combine the booking of
+beds with the registration process. There are two major use cases.
+Students either have to book a bed space before they can continue
+with the registration, or they are only allowed to book, if they
+have reached a certain registration state. In the first case, the
+university requires students to stay on campus. They usually have
+enough beds for all of their students and need to fill up the
+hostels. In the second case, bed space is limited. Since
+accommodation on campus is usually very reasonable and much safer
+than on the free market outside, students prefer to stay on campus.
+If bed allocation e.g. requires the payment of school fees, the
+university benefits from bed shortage, because students are forced
+to pay in order to get the highly coveted hostel bed.
+
+From the database's point of view, the accommodation section is a
+container of type `HostelsContainer` with id ``hostels``, which is
+located in the `IUniversity` instance. It contains hostels
+(instances of `IHostel`) which again contain the beds (instances of
+`IBed`).
+
+The :ref:`treelike storage of objects <object_database>` in the
+accommodation section can be figured as follows::
+
+
+  Accommodation Section (HostelsContainer)
+  |
+  +---> Hostel
+        |
+        +---> Bed
+
+Interfaces
+==========
+
+`IHostelsContainer`
+-------------------
+
+The unique hostels container serves also as a configuration object.
+It defines for which academic session booking is enabled. The
+student's `current_session` must match the `accommodation_session`.
+It also defines the booking period and the registration workflow
+states for which booking is allowed, see :ref:`bed_tickets`. The
+only property attribute is `expired` which returns ``True`` if the
+current datetime is within the booking period.
+
+.. literalinclude:: ../../../src/waeup/kofa/hostels/interfaces.py
+   :pyobject: IHostelsContainer
+
+`IHostel`
+---------
+
+The hostels container contains the various `Hostel` objects. Hostels
+are buildings with blocks, floors rooms and beds. When adding a
+hostel, the form page is requesting the hostel's name. The
+`hostel_id` is derived from the name by applying
+:code:`lower().replace(' ','-').replace('_','-')` to it. As usual,
+the id will be omitted in manage forms and can thus not be changed
+after hostel creation.
+
+The add and manage form pages let us define the 'dimensions' of the
+hostel and configure blocks with their assignment to either
+female or male students. And they are requesting the number of
+floors per block as well as the number of rooms per floor. All
+blocks have the same number of floors with a fixed number of rooms
+and a fixed number of beds per room. If beds or even entire rooms do
+not exist on a floor, these beds must be later marked reserved, so
+that they are skipped during the automatic allocation process, see
+below.
+
+.. note::
+
+  Blocks for either female or male students? Does Kofa's hostel
+  management accommodate girls and boys strictly separately? No,
+  it doesn't. Blocks are not necessarily real buildings. They can
+  be used as virtual subunits. If the entire hostel has only one
+  block, then yes, either only female or only male students can be
+  hosted in such a hostel. If two blocks are configured (one for
+  male and one for female students), beds of the same room can be
+  assigned twice, once in Block A and a second time in Block B.
+  Consequently, one bed of such a bed pair must be set 'reserved'
+  (see below) to avoid double allocation. In other words, there is
+  always a trick which allows even such uncommon configurations.
+  Not many universities will allow girls and boys to stay in the
+  same room.
+
+.. literalinclude:: ../../../src/waeup/kofa/hostels/interfaces.py
+   :pyobject: IHostel
+
+Beds can be dedicated to pre-study students, fresh students
+(`entry_session` and the hostels container's `accommodation_session`
+correspond), final-year students (`current_level` and the
+certificate's `end_level` correspond, or is even higher) and
+returning students (non-fresh and non-final-year students). Or they
+can be made bookable for all students (beds without category). The
+latter bed type can be configured but is not being used in the base
+package by
+:py:meth:`getAccommodationDetails<waeup.kofa.students.utils.StudentsUtils.getAccommodationDetails>`,
+the method which determines the appropriate bed type of the
+student.
+
+Usually, not every student can be accommodated in every hostel.
+Faculties are sometimes far apart and do manage their own student
+hostels. These hostels require a 'special handling' in
+:py:meth:`getAccommodationDetails<waeup.kofa.students.utils.StudentsUtils.getAccommodationDetails>`.
+The special handling code must be set on the add and manage form
+pages of hostels.
+
+The interface defines also two schema invariants
+(invariant-decorated methods). These methods validate one or more
+depending schema fields. In our case, the methods take care that
+blocks and beds are not assigned twice.
+
+All the parameters above define the construction rules for beds when
+filling the hostel with beds, which is done by the `updateBeds`
+method described further below.
+
+`IBed`
+------
+
+.. literalinclude:: ../../../src/waeup/kofa/hostels/interfaces.py
+   :pyobject: IBed
+
+The `bed_id` contains the 'coordinates' of the bed. It tells us
+precisely in which hostel, in which block, on which floor and in
+which room the bed can be found. The bed id is composed as follows:
+``[hostel id]_[block letter]_[room number]_[bed letter]``. The room
+number contains the floor level: :code:`room_nr = floor*100 + room`.
+Example: ``hall-1_A_101_C`` means that the bed is located in Hostel
+1, Block A, 1st Floor, Room 101 (or 1) and labelled with 'C'.
+
+The `bed_type` attribute is similarly being constructed. It
+describes which kind of student can be allocated: ``[special
+handling code]_[sex]_[stage]``. Example: ``regular_female_re`` means
+that this bed can be booked by regular female returning students.
+Other stages are: ``fr`` (fresh), ``pr`` (pre-study), ``fi``
+(final-year) and ``all`` (all students).
+
+Beds of each hostel are consecutively numbered (`bed_number`).
+
+Except for `owner`, all attributes of bed objects are being
+determined by the system, no matter if they are property or schema
+field attrributes. They can neither be edited nor imported (there is
+no batch processor for beds). The `owner` attribute contains the
+student id, if the bed is occupied. This attribute is either set by
+Kofa when the student creates a bed ticket (see :ref:`bed_tickets`),
+or can be set via the `BedManageFormPage`, see below. The
+`allowed_owners` schema invariant ensures (1) that the selected user
+exists, (2) that the student's current session corresponds with the
+accommodation session and (3) that the student doesn't reside in
+another bed.
+
+.. _hostels_pages:
+
+Browser Pages
+=============
+
+.. seealso::
+
+   :ref:`Manage Hostel Python Test <test_manage_hostels>`
+
+Update Beds
+-----------
+
+Hostels are empty after creation and configuration. They do not
+contain any bed. The hostel's
+:py:meth:`Hostel.updateBeds<waeup.kofa.hostels.hostel.Hostel.updateBeds>`
+method must be called to fill the hostel with beds according to
+the hostel's configuration parameters. This is done by the
+same-named action of the `HostelManageFormPage`. `Hostel.updateBeds`
+iterates over all block letters, floor levels, room numbers and bed
+letters, which have been configured on the `HostelManageFormPage`.
+In each bed letter iteration loop a bed is added and the `bed_id`,
+`bed type` as well as the consecutive `bed_number` are determined
+and stored with the bed.
+
+As the method's name already promises, it does not only add beds to
+an empty hostel container, but also updates existing beds after
+re-configuration. It does this by removing all empty beds before the
+iteration starts. Occupied beds remain in the hostel but get the bed
+number ``9999``. When iterating over the newly configured blocks,
+floors, rooms and bed letters, Kofa checks first, if the bed belongs
+to the group of remaining beds, which could not be removed because
+they are occupied. If so, the bed type is adjusted and the bed
+number changed. The bed remains occupied by the same student, no
+matter if the student meets the newly configured conditions or not.
+
+It might happen that e.g. a room for male students is converted into
+a room for female students, but a male student still resides in this
+room. This has to be checked and the student has to be relocated
+manually, see :ref:`student_relocation`. Moreover, due to the
+reconfiguration of the hostel, an occupied bed may no longer exist
+or offered for booking. This is then indicated by the bed number
+``9999``. Also these students must be relocated manually.
+
+
+Switch Reservations
+-------------------
+
+Beds can be blocked by switching ther reservation status. The switch
+replaces the stage part of `bed_type` by ``_reserved``. Example:
+``regular_female_re`` becomes ``regular_female_reserved``. Switching
+again, reverts this process.
+
+Reserved beds won't be allocated automatically. The beds remain
+empty during the hostel booking period. Students can only be
+allocated manually to reserved beds, which is done via the
+`BedManageFormPage`, see below.
+
+Release Beds
+------------
+
+Releasing a bed does not simply mean clearing the `owner` attribute
+of the bed object. The student has to be notified that booking has
+been cancelled. This is done by replacing the `bed_coordinates` of
+the student's bed ticket. Instead of the coordinates, the student
+will read: ``-- booking cancelled on <datetime of cancellation> --``.
+
+Manage Bed
+----------
+
+Not many things can be managed on this form page. As mentioned above,
+only the `owner` attribute can be changed by entering a student id.
+The id is being validated by a schema invariant. The manage form
+page cannot be accessed, if a student has aleady been allocated.
+Beds must be released first, before they can be allocated to other
+students.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/index.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/index.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/index.rst	(revision 16045)
@@ -0,0 +1,23 @@
+User Handbook
+*************
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   browsing
+   academics
+   students
+   applicants
+   hostels
+   documents
+   datacenter
+   accesscodes
+   security
+   users
+   reports
+   mandates
+   configuration
+   customization
+   testing
+   buildingdocs
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/install.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/install.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/install.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _installing_linux:
+
+.. include:: ../../INSTALL.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/mandates.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/mandates.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/mandates.rst	(revision 16045)
@@ -0,0 +1,4 @@
+.. _mandates:
+
+Mandates :sup:`in progress`
+***************************
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/reports.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/reports.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/reports.rst	(revision 16045)
@@ -0,0 +1,4 @@
+.. _reports:
+
+Reports :sup:`in progress`
+**************************
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/security.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/security.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/security.rst	(revision 16045)
@@ -0,0 +1,349 @@
+.. _security_policy:
+
+Security
+********
+
+.. seealso::
+
+   :ref:`Security Doctests <security_txt>`
+
+Kofa has a very efficient security machinery. The machinery does not
+perform authorization checks on the content objects themselves stored
+in the database, but, restricts the usage of views, i.e. web pages and
+forms which are needed to view or edit data. Views are protected by
+permissions the user must have to use the view. Instead of assigning
+permissions seperately to users, permissions are bundled into sets of
+permissions, so-called roles which can be assigned to users through
+the web interface.
+
+It is important to note that permissions do not include other
+permissions. Only roles 'include' permissions. A 'manage' permission,
+for example, does not automatically enable users to open pages which
+merely display the data. These pages have their own 'view'
+permission. Another example is the ManagePortal permission described
+below. The name of the permission may lead to believe that users can
+do everything with this permissions. This is not true. It does only
+give access to certain pages which are dedicated to portal managers
+and must not be accessed by any other user.
+
+.. contents:: Table of Contents
+   :local:
+
+Permissions
+===========
+
+The whole set of permission and role classes are described in the
+:py:mod:`Permissions and Roles Module<waeup.kofa.permissions>`. Here
+we describe only a subset of permission classes which are essential
+for the security settings configuration.
+
+General Permissions
+-------------------
+
+.. autoclass:: waeup.kofa.permissions.Public()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.Anonymous()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.Authenticated()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ManageUsers()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.EditUser()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ManagePortal()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ViewAcademics()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ManageAcademics()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ManagePortalConfiguration()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ManageDataCenter()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ExportData()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ImportData()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.TriggerTransition()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ShowStudents()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ExportPaymentsOverview()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ExportBursaryData()
+   :noindex:
+
+.. autoclass:: waeup.kofa.reports.HandleReports()
+   :noindex:
+
+.. autoclass:: waeup.kofa.reports.ManageReports()
+   :noindex:
+
+Accommodation Section Permissions
+---------------------------------
+
+.. autoclass:: waeup.kofa.hostels.permissions.ViewHostels()
+   :noindex:
+
+.. autoclass:: waeup.kofa.hostels.permissions.ManageHostels()
+   :noindex:
+
+.. autoclass:: waeup.kofa.hostels.permissions.ExportAccommodationData()
+   :noindex:
+
+Applicants Section Permissions
+------------------------------
+
+.. autoclass:: waeup.kofa.applicants.permissions.ViewApplication()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.HandleApplication()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.ManageApplication()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.PayApplicant()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.ViewApplicationStatistics()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.CreateStudents()
+   :noindex:
+
+Students Section Permissions
+----------------------------
+
+.. autoclass:: waeup.kofa.students.permissions.ViewStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.HandleStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.ViewStudentsContainer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.ManageStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.PayStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.HandleAccommodation()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.UploadStudentFile()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.LoginAsStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.EditStudyLevel()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.ClearStudent()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.ValidateStudent()
+   :noindex:
+
+Global Roles
+============
+
+Global or site roles are assigned portal-wide. In contrast to local
+roles, users have this role in every context.
+
+Many global roles do only bundle one or two permissions. The objective
+behind is to share responsibilities and distribute tasks.
+
+Global roles are being assigned via the user manage form page.
+
+Global General Roles
+--------------------
+
+.. autoclass:: waeup.kofa.permissions.AcademicsOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.AcademicsManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.DataCenterManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ImportManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ExportManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.ACManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.UsersManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.WorkflowManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.BursaryOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.reports.ReportsOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.reports.ReportsManager()
+   :noindex:
+
+In contrast to these specialized sets of permissions, there are two
+sets which delegate extensive powers on portal managers.
+
+.. autoclass:: waeup.kofa.permissions.PortalManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.CCOfficer()
+   :noindex:
+
+Global Accommodation Section Roles
+----------------------------------
+
+.. autoclass:: waeup.kofa.hostels.permissions.AccommodationOfficer()
+   :noindex:
+
+Global Applicants Section Roles
+-------------------------------
+
+Global Applicants Section Roles are assigned portal-wide (globally)
+but do actually only allocate permissions in the applicants section.
+
+.. autoclass:: waeup.kofa.applicants.permissions.ApplicantRole()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.ApplicationsOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.ApplicationsManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.StudentsCreator()
+   :noindex:
+
+Global Students Section Roles
+-----------------------------
+
+Global Students Section Roles are assigned portal-wide (globally) but
+do actually only allocate permissions in the students section.
+
+.. autoclass:: waeup.kofa.students.permissions.StudentRole()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentsOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentsManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentsClearanceOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentsCourseAdviser()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentImpersonator()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.TranscriptOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.TranscriptSignee()
+   :noindex:
+
+.. _local_roles:
+
+Local Roles and Dynamic Role Assignment
+=======================================
+
+In contrast to global roles, which are assigned portal-wide, local
+role permissions are gained for a specific context.
+
+Some local roles serve a second purpose. At first glance it appears
+strange that some of these 'odd' roles do not give more permissions
+than the user already has due to other roles. Their real purpose is to
+delegate permissions to the students or applicants section. If a user
+has for example the LocalStudentsManager role described below at
+department level, s/he automatically gets the StudentsManager role for
+those students studying in this department. We call this a **dynamic
+role**. In contrast to static global or local roles, dynamic roles are
+not stored in the database, they are dynamically assigned.
+
+Local roles are assigned either automatically by the system during
+user object setup or manually through the web interface. The
+automatically assigned local roles are:
+
+.. autoclass:: waeup.kofa.permissions.Owner()
+   :noindex:
+
+.. autoclass:: waeup.kofa.applicants.permissions.ApplicationOwner()
+   :noindex:
+
+.. autoclass:: waeup.kofa.students.permissions.StudentRecordOwner()
+   :noindex:
+
+All other local roles must be assigned manually via context manage form pages.
+
+.. autoclass:: waeup.kofa.permissions.ApplicationsManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.DepartmentOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.DepartmentManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.Lecturer()
+   :noindex:
+
+The following local roles do also delegate permissions to the student
+section. In other words, dynamic roles are assigned.
+
+.. autoclass:: waeup.kofa.permissions.ClearanceOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.LocalStudentsManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.LocalWorkflowManager()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.LocalReportsOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.UGClearanceOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.PGClearanceOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.CourseAdviser100()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.LocalTranscriptOfficer()
+   :noindex:
+
+.. autoclass:: waeup.kofa.permissions.LocalTranscriptSignee()
+   :noindex:
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students.rst	(revision 16045)
@@ -0,0 +1,72 @@
+.. _student_section:
+
+Students Section
+****************
+
+'Student' is a multi-purpose term in Kofa which may lead to some
+misconceptions. First of all, 'Student' is a Python/Grok container
+class and student objects are the instances of this class. Sometimes
+we are sluggish when talking about 'creating students' and actually
+mean: 'creating student container objects and storing the objects
+(persistently) in the ``students`` container'. The latter is the
+parent container for all students in the portal.
+
+The :ref:`treelike storage of objects <object_database>` in the
+students section can be figured as follows::
+
+
+  Students Section (StudentsContainer)
+  |
+  +---> Student
+        |
+        +---> StudentStudyCourse
+        |     |
+        |     +-----> StudentStudyLevel
+        |             |
+        |             +-----> CourseTicket
+        |
+        +---> StudentPaymentsContainer
+        |     |
+        |     +-----> StudentOnlinePayment
+        |
+        +---> StudentAccommodation
+              |
+              +-----> BedTicket
+
+
+.. note::
+
+  Throughout Kofa we distinguish singular and plural expressions. A
+  students container contains students (or more precisely student
+  containers), a payments container contains payments, a courses
+  container contains courses, a certificates container contains
+  certificates.
+
+Furthermore, a student is also a user of the portal and the student
+object is a user account surrogate, see :ref:`Applicants and
+Students <applicants_and_students>`. In the following we always
+refer to the student container when talking about a student.
+
+Interfaces
+==========
+
+.. toctree::
+   :maxdepth: 3
+
+   students/interfaces
+
+Workflow & History
+==================
+
+.. toctree::
+   :maxdepth: 3
+
+   students/workflow
+
+Browser Pages
+=============
+
+.. toctree::
+   :maxdepth: 3
+
+   students/browser
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/browser.rst	(revision 16045)
@@ -0,0 +1,442 @@
+.. _logging_in_as_student:
+
+Impersonate Students
+====================
+
+Officers with
+:py:class:`LoginAsStudent<waeup.kofa.students.permissions.LoginAsStudent>`
+permission do see a 'Login as student' button on student display
+pages which redirects to a page, where a temporary student password
+can be set by clicking a 'Set password now' form button. The
+temporary password is valid for 10 minutes. During this period the
+student can't login. The officer is being redirected to a login page
+which allows to directly login as student with pre-filled temporary
+credentials. The second page is to avoid that officers must remember
+the student credentials, have to logout from their own account and
+manually login as student via the regular login page. After 10
+minutes (default value) the officer is automatically thrown out and
+the student is able to login again (if the officer hasn't changed
+the password). Attention: When logging in as student, the officer
+really impersonates the student. All actions are logged with the
+student id and not with the officer's user id. However, start of
+student impersonation is also logged, so that officers can be
+identified and fraudulent use can be discovered.
+
+
+.. _parents_access:
+
+Parents Access
+==============
+
+Parents may gain access to monitor study progress and view all
+payments made by their children. They won't be able to edit data,
+register courses or make payments. Prerequisite for getting access
+is that they have a valid email address and that this address has
+been entered by the student into the parents' email address field.
+If so, parents can request a password on the
+:py:class:`RequestParentsPasswordPage<waeup.kofa.students.browser.RequestParentsPasswordPage>`.
+The student record will be looked up and temporary login
+credentials will be sent to the parents' email address. If they
+click on the confirmation (mandate) link in this email, the
+temporary password will be activated so that they can login. Access
+will be granted for 10 minutes (default value). Parents don't have
+their own account in Kofa. They are logging into the student's
+account instead. The student's password is inactive during this
+period. After 10 minutes parents are automatically thrown out and
+the student is able to login again with the original password.
+
+
+.. _starting_clearance:
+
+Starting Clearance
+==================
+
+When students start the clearance process, Kofa checks if the
+passport picture has been uploaded and the email address field as
+well as the phone number field filled. If not, students can't
+proceed with clearance.
+
+
+.. _rejecting_clearance:
+
+Rejecting Clearance
+===================
+
+When a clearance officer clicks the 'Save comment and reject
+clearance now' button, the `reject` action method of the
+:py:class:`StudentRejectClearancePage<waeup.kofa.students.browser.StudentRejectClearancePage>`
+is called. This method first checks, in which workflow state the
+student is, and fires a transition accordingly. Then the comment,
+which should explain why the request was requested, is saved twice
+in the `officer_comment` attribute of the student and in
+``students.log``. As soon as the `clear` transition (d) is effected,
+an event handler clears the attribute again. The logfile message is
+permanent and ensures that the original cause of rejection can
+always be reconstructed.
+
+Finally, the method redirects to the `ContactStudentFormPage` and prefills
+the HTML form with the comment previously saved. The clearance
+officer can leave the comment as it is, or can modify the text to
+give more information about the reason of rejection. This comment
+will then be sent to the student by email. This comment is not saved,
+neither in the student object nor in the logfile.
+
+.. important::
+
+  All messages sent via Kofa contact forms are private. They are
+  neither stored in the database nor in any logfile. The emails are
+  also not forwarded to any other email address. Thus senders and
+  recipients can be sure that nobody else is eavesdropping and the
+  correspondence is kept secret.
+
+.. seealso::
+
+  :ref:`Clearance Handling Tests <test_handle_clearance>`
+
+
+.. _transferring_students:
+
+Student Transfer
+================
+
+Transferring a student means enabling the student to study another
+programme.
+
+The simple but dirty way is to select another certificate and adjust
+study course attributes accordingly. Existing study levels can be
+either removed or, in case registered courses can be credited, left
+as they are. This simple way is tedious and also dangerous, because
+changes are not tracable. Only the logfile can tell us, that an
+officer has edited the student's data. The data of the previous
+study course are not backed up. They are lost.
+
+Kofa provides a more adequate, cleaner and tracable way of
+transferring students. It can make a backup of the entire study
+course container and create a new and empty container for the new
+study programme with only one click or even by batch processing.
+
+After clicking the 'Transfer student' button the
+`StudentTransferFormPage` opens and asks for the new certificate,
+current session, current level and current verdict. After submitting
+this form, the student transfer method checks if the old study
+course is complete and ready for transfer. It also checks if the
+number of possible transfers is not exceeded. Kofa allows two (2)
+transfers! Finally the copying process is started and history and
+logfile messages are added.
+
+The old study course container(s) can still be accessed via links on
+the current study course display form page. But, they can neither be
+edited, removed, exported or reimported.
+
+
+Batch Transferring Students
+---------------------------
+
+Sometimes students of an entire study programme have to be
+transferred to another programme. This can be done with the
+:ref:`student_study_course_processor`. The import file must contain
+the following columns: `student_id` (or any other locator),
+`certificate`, `current_session`, `current_level` and `entry_mode`.
+Do not import `entry_session`. A transfer is automatically
+initialized if the `entry_mode` value is ``transfer``.
+
+
+Reverting Previous Transfers
+----------------------------
+
+Previous transfers can be reverted by opening the previous study
+course and clicking the 'Reactivate this study course (revert
+previous transfer)' button. This is a complete rollback of the last
+transfer. The current study mode will be irrevocably deleted and
+replaced by the previous study course. The second last study course
+will become the previous study course.
+
+
+.. _student_payment_tickets:
+
+Payments
+========
+
+The `PaymentsManageFormPage` is used by both students and students
+officers. The page tabulates existing payment tickets and allows to
+add or remove tickets. Officers can remove all payment tickets,
+students only those without a response code (`r_code`). Attention:
+Students can remove tickets without response code even if they have
+been marked paid.
+
+There are three different add form pages to add
+`StudentOnlinePayment` instances (= payment tickets). They all
+create objects of the same type, only their attributes are set
+differently.
+
+
+Current Session Payment Tickets
+-------------------------------
+
+Current session payments are the regular payments which have to be
+made in each session to proceed to the next registration step. The
+add form provides a select box of available payment categories
+(`p_category`). After submitting the form, Kofa determines the total
+amount and sets attributes like payment item (`p_item`), payment
+session (`p_session`) and payment level (`p_level`) automatically.
+The Boolean `p_current` attribute is set ``True``. The creation
+datetime is stored in the `creation_date` attribute and is also used
+to construct the unique payment id (`p_id`).
+
+.. note::
+
+  Kofa always determines the total amount, including any fees charged
+  by the school and its service providers. This is the amount which is
+  authorized by students and finally submitted to one of the payment
+  gateways. No fees can be added once the payment ticket is created.
+  Payment tickets do not store any information about charged fees.
+
+
+Payment Ticket Redemption
+-------------------------
+
+Directly after a student payment ticket has been paid - either by
+approval by an officer or by receiving a positive response from a
+payment gateway - the
+:py:meth:`redeemTicket<waeup.kofa.students.payments.StudentOnlinePayment.redeemTicket>`
+method is called. Depending on the category of the payment, an
+appropriate access or activation code is beeing created for the
+owner of the ticket. This code must be entered on certain form pages
+to activate the paid service or to access the next stage of the
+registration process. In other words, making a payment and redeeming
+a payment are two different steps. Successful payments do not
+automatically trigger any action in the portal but create a specifc
+access code which can be used to trigger access-code-related actions
+(see :ref:`accesscodes`).
+
+Until May 2015 also school fee payments had produced access codes,
+which enabled students to start the next session. Since software
+revision 12889, Kofa bypasses SFE access code creation and starts
+the next session automatically.
+
+
+Previous Session Payment Tickets
+--------------------------------
+
+Previous session payments are additional payments which do not
+induce further actions in Kofa. Their sole purpose is to enable
+students to pay for services in previous sessions which they missed
+to pay. The add form for previous session payments allows the
+student to select the payment category, session and level by
+him/herself.
+
+
+Balance Payment Tickets
+-----------------------
+
+Balance payments have been introduced to correct previously made
+payments. In some cases, students select the wrong payment category,
+or other things may have happened which led students pay less than
+expected. This can be balanced by paying a differential amount.
+Therefore, the add form for balance payments allows to freely choose
+the total amount to be paid. It also asks for the category, the
+session and the level the payment is meant for. Like previous
+session payments, balance payments do not induce further actions in
+Kofa. Both can be omitted in customized versions of Kofa if these
+features are not needed.
+
+.. _course_registration:
+
+Course Registration
+===================
+
+Study levels are pre-filled with course tickets. When adding a study
+level,
+:py:meth:`StudentStudyCourse.addStudentStudyLevel<waeup.kofa.students.studycourse.StudentStudyCourse.addStudentStudyLevel>`
+automatically adds course tickets in two steps:
+
+1.  :py:meth:`StudentStudyLevel.addCertCourseTickets<waeup.kofa.students.studylevel.StudentStudyLevel.addCertCourseTickets>`
+    is called which iterates over the certificate courses of the
+    certificate container object in the academic section and creates
+    course tickets if the `level` attribute matches. `title`, `fcode`,
+    `dcode`, `credits`, `passmark` and `semester` are copied from the
+    course object which is attached to the certificate course;
+    `mandatory` and `course_category` are taken from the certificate
+    course itself. Finally, `automatic` is set to ``True`` and
+    `carry_over` to ``False.``
+
+2. The portal can be configured
+   (`IConfigurationContainer.carry_over`) such that failed courses
+   are automatically carried over from one session to the next.
+   Failed course tickets from the previous level, i.e. tickets
+   with a score below the passmark, are collected and 'copied'
+   into the current study level container. The attributes
+   `automatic` and `carry_over` are set to ``True``.
+
+In most cases such an automatically created course list is not
+perfect or even ready for submission to the course adviser. The list
+must be edited according to the student's needs. Students can select
+further courses, which they desire to attend, and can create
+additional course tickets, as long as the total number of credits of
+non-outstanding courses (`outstanding` attribute is ``True``) do
+not exceed 50 (value customizable). That means outstanding courses
+are not considered as registered courses. Usually they are being
+added by officers.
+
+Course tickets can also be removed. Whereas officers can remove any
+ticket from the list, students can remove only optional
+(non-mandatory) course tickets (condition customizable).
+
+The edit form page provides two additional buttons. 'Update all
+tickets' ignores the select boxes and checks all course ticket at
+that level. It looks up the associated course object for each ticket
+and updates the ticket's course parameters (including course title)
+if needed. Attention: If a course was removed, the associated
+course ticket will be invalidated by adding '(course cancelled)' to
+the title and setting the credits to zero. The 'Register course
+list' button submits the course list to the course adviser for
+validation. If the course registration deadline
+(`ISessionConfiguration.coursereg_deadline`) is set and the
+registration period has expired, a late registration fee
+(`ISessionConfiguration.late_registration_fee`) is charged. This
+payment has to be made first, otherwise a warning message appears in
+the browser.
+
+Course advisers can't edit the registered/submitted course list, but
+they can validate or reject it by pressing the same-named link
+buttons. After pressing the 'Reject courses' button, Kofa redirects
+to the `ContactStudentFormPage` which can be used to inform the
+student about the reason of rejection. In contrast to clearance
+rejection, the message, which is being sent to the student by email,
+is neither stored in the database nor in the logfiles.
+
+.. seealso::
+
+  :ref:`Course List Validation Tests <test_handle_courses>`
+
+
+.. _batch_editing_scores:
+
+Batch Editing Scores by Lecturers
+=================================
+
+Lecturers cannot access student records directly. They don't have
+access to the students section. Instead, lecturers go to their
+course in the academic section and click the 'Update scores' button
+which opens the `EditScoresPage` if score editing is enabled for
+that department (`IDepartment.score_editing_disabled`) and
+`IConfigurationContainer.current_academic_session` has been set on
+the portal's configuration page. The `EditScoresPage` lists all
+students, who are attending the course in the current academic
+session. Score editing is allowed if the student's current session
+corresponds with the current academic session and the student is in
+state 'courses validated', see method
+:py:meth:`CourseTicket.editable_by_lecturer<waeup.kofa.students.studylevel.CourseTicket.editable_by_lecturer>`.
+
+There are two options to edit course results. (1) Scores in course
+tickets can be changed by editing its values in the table and
+pressing the 'Update scores from table' button below. Scores can be
+cleared by removing the respective values. Lecturers have to be
+online during this process. (2) Alternatively, lecturers can download
+a csv file, edit scores in this csv file offline and upload the same
+file when they are online again. This procedure is explained in
+step-by-step instructions which show up when pressing the yellow
+'Help' button:
+
+.. admonition:: Help
+
+  **Step-by-step instructions**
+
+  1. Download csv file.
+  2. Open csv file in a text editor or in a spreadsheet programme
+     (Excel or Calc).
+  3. Edit course results only. Do not modify other entries.
+     Do not remove or add columns. Do not add rows.
+  4. Save file in same format (csv). Do not switch to any other
+     format (xls, xlsx or ods).
+  5. Select same file for upload and press the blue 'Update ...'
+     button.
+  6. The values in the table will be updated. Spot-check if the
+     values in the table correspond with the values in your file.
+
+  Note: Only course results of students which are in state
+  'courses validated' and in current academic session can be modified.
+  Additional data will just be ignored.
+
+.. seealso::
+
+  :ref:`Batch Editing Scores Tests <test_batch_editing_scores>`
+
+
+.. _bed_tickets:
+
+Bed Tickets
+===========
+
+.. _bed_allocation:
+
+Bed Allocation
+--------------
+
+Students can obtain a bed ticket if a series of conditions is met:
+
+- The current date must be inside the booking period (between
+  `IHostelsContainer.startdate` and `IHostelsContainer.enddate`).
+
+- The student's current session must match the accommodation session
+  (`IHostelsContainer.accommodation_session`).
+
+- A bed ticket for the same accommodation session does not exist.
+
+- The student must be in the correct workflow state
+  (`IHostelsContainer.accommodation_states`).
+
+- A bed type, which fits to the student, can be determined.
+
+- A bed of that type is available.
+
+- The HOS activation code is not yet used.
+
+- The student is the owner of the activation code.
+
+The customizable utility method
+:py:meth:`getAccommodationDetails<waeup.kofa.students.utils.StudentsUtils.getAccommodationDetails>`
+composes a bed type string. Three criteria are checked: Is the
+student a new, a returning or a final year student? Is the student
+female or male? Has the student to be accommodated in a special
+hostel (`IHostel.special_handling`)? The resulting bed type string
+contains these information. Example: ``regular_female_fr`` means
+that a bed for a new female student in a regular hostel is wanted.
+If the student record allows to determine such a bed string, Kofa
+starts searching a proper bed space.
+
+Before Kofa searches for a free bed space, which meets the bed type
+criteria above, it checks if a bed space has already been allocated
+manually to the student. If so, then this bed is used, no matter
+whether the bed meets the criteria or not. (Theoretically, a male
+student can be accommodated in a hostel which is reserved for female
+students.) If no manually allocated bed space is found, Kofa
+searches for the right space. If bed booking is subject to a charge,
+Kofa also checks, if the student has entered a valid activation code,
+before delivering the bed coordinates to the student.
+
+
+.. _student_relocation:
+
+Student Relocation
+------------------
+
+Officers with `ManageHostels` permission do see a 'Relocate student'
+link button which calls the `BedTicketRelocationView`. This view
+relocates the student if student parameters or the bed type of the
+bed have changed. The `update` method of this view calls the
+:py:meth:`BedTicket.relocateStudent<waeup.kofa.students.accommodation.BedTicket.relocateStudent>`
+method which checks first, if the student has a 'reserved' bed
+space. Students in reserved beds are never subject to relocation. It
+checks secondly, if booking has been cancelled in the accommodation
+section but other bed space has been manually allocated after
+cancellation. Then this bed is used, no matter whether the bed meets
+the bed type criteria or not. If both checks are negative, Kofa
+searches for a free bed space, which meets the student's bed type
+criteria. Only if it finds a new and free bed space, it starts the
+relocation process by releasing the old bed, booking the new bed and
+designating the new bed in the bed ticket.
+
+.. seealso::
+
+  :ref:`Bed Space Booking Tests <test_handle_accommodation>`
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/interfaces.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/interfaces.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/interfaces.rst	(revision 16045)
@@ -0,0 +1,354 @@
+.. _students_interfaces:
+
+Student Interfaces
+==================
+
+The `Student` class is a container class which means, there are not
+only attributes describing the student entity but also content. Each
+student container contains three subcontainers:
+``studycourse`` (instance of `StudentStudyCourse`), ``payments``
+(instance of `StudentPaymentsContainer`) and ``accommodation``
+(instance of `StudentAccommodation`). The purposes of them are
+described further below. The student container with all its
+subcontainers and subobjects is called **student record**.
+
+Let's talk about the attributes and methods belonging to the
+`Student` class, the so-called 'external behaviour' of student
+objects specified in Zope 'interfaces'. The data stored with each
+student object are subdivided into three parts: base data, personal
+data and clearance data. Each part has its own interface.
+
+.. _kofa_interfaces:
+
+.. note::
+
+  **Interfaces** are one of the pillars of Zope's Component
+  Architecture (ZCA, see also :ref:`prerequisites`). They document the
+  'external behaviour' of objects. In Kofa interfaces are used to
+  specify the attributes, methods and schema fields of objects. The
+  first two are well-known Python concepts. A Zope schema field is a
+  bit more complicated. It's a detailed description of a field to be
+  submitted via forms to the server, or which is processed by a batch
+  processor. In both cases the input data are being validated against
+  the schema field requirements. In Kofa, schema fields (see also note
+  below) in interfaces are also used to automtically add
+  `FieldProperty` attributes of the same name to most content classes.
+  This is done by a function
+  :py:func:`attrs_to_fields<waeup.kofa.utils.helpers.attrs_to_fields>`
+  which is called once during startup of Kofa. These class attributes
+  ensure that corresponding attributes of instances of this class -
+  when being added or changed - always meet the requirements of the
+  schema field. Another big advantage of such class attributes is,
+  that attributes with a `FieldProperty` do not need to be set in
+  `__init__` methods.
+
+The `Student` class implements several interfaces which we will
+quote to unveil the student's 'external behaviour'.
+
+`IStudentBase`
+--------------
+
+`waeup.kofa.students.interfaces.IStudentBase` defines a fundamental
+set of attributes, schema fields and methods which are being used
+throughout the portal. This set is immutable. No class statement
+must be removed or added in customized versions of Kofa.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentBase
+
+The **first part** of the interface lists attributes. Except for the
+last two attributes (`password` and `temp_password`) they are all
+read-only property attributes, i.e. attributes with a getter method
+only. These properties are computed dynamically and can't be set.
+Most of them return data derived from the ``studycourse`` subobject.
+
+The **second part** of the interface specifies the schema fields.
+
+.. _kofa_schemas:
+
+.. note::
+
+  **Schema fields** are instances of schema field classes with `title`,
+  `description`, `default`, `required`, `readonly` attributes. Class
+  names and class attributes are self-explanatory and described in the
+  `Zope Schema`_ documentation.
+
+  A very powerful Zope Schema field is `Choice`. Choice fields are
+  constrained to values drawn from a specified set, which can be
+  static or dynamic. This set can be a simple list of choices (=
+  `values`), a `vocabulary` or a `source` which produce dynamic
+  choices.
+
+  **Vocabularies and sources** are very similar. The latter is a newer
+  concept of producing choices and considered to replace vocabularies.
+  In Kofa we use both. Common to both is, that a set of values is
+  accompanied by a user-friendly title, which is displayed on pages
+  (e.g. in select boxes) instead of a less informative value token or
+  even a cryptic representation of a persistent value object. In most
+  cases we have a token-title pair which perfectly describes the
+  values of a source or a vocabulary. The tokens are being sent in
+  forms or imported by batch processors.
+
+  See `token-title pairs <http://kofa-demo.waeup.org/sources>`_ of
+  most (non-database-dependent) sources and vocabularies in the Kofa
+  base package.
+
+The **third part** of the interface lists the methods which can be
+applied to student objects.
+
+`IUGStudentClearance`
+---------------------
+
+Much like all the following student interfaces,
+`waeup.kofa.students.interfaces.IUGStudentClearance` describes an
+additional set of schema fields which are exclusively used on form
+pages. They do not play any role beyond that purpose. Any kind of
+schema field can be safely added in customized versions of these
+interfaces to meet the conditions of the university's matriculation
+process. Also `date_of_birth` and `nationality` are not used in the
+base package for further data processing which means, there is no
+dependency on these two parameters. Attention: This might be
+different in customized versions of Kofa. Some views might use these
+parameters to restrict access or make actions (e.g. payment of items)
+conditional on the student's nationality or current age.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IUGStudentClearance
+
+`clearance_locked` controls the access to the edit form page of
+clearance data. The corresponding attribute is automatically set by
+workflow transitions to lock or unlock the edit form page
+respectively. The attribute can also be set by officers via the
+manage form page to lock or unlock the page manually.
+
+`officer_comment` is usually edited by clearance officers when
+rejecting clearance previously requested by the student. The
+attribute contains the message which informs the student about the
+reasons of rejection. The attribute is cleared after final
+clearance. The message can be also found in ``students.log``.
+
+Both `clearance_locked` and `officer_comment` are controlled by
+workflow transitions not by states, see below.
+
+`IPGStudentClearance`
+---------------------
+
+Most universities acquire different data from undergraduate (UG) and
+postgraduate (PG) students. The forms vary widely. Therefore Kofa
+offers a second interface for acquiring PG clearance data. In the
+base package `IPGStudentClearance` inherits from the
+`IUGStudentClearance` interface. The additional `employer` field in
+the base package only serves as an example.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IPGStudentClearance
+
+`IStudentPersonal`
+------------------
+
+`waeup.kofa.students.interfaces.IUGStudentPersonal` defines an
+additional set of personal student data which are permantly editable
+by students. The set usually contains further contact data and
+information about the student's relatives.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentPersonal
+
+Kofa provides a mechanism which ensures that personal data are being
+kept up to date. Students are regularly requested to update the
+personal data directly after login. The personal data expire 180
+days after saving the data for the last time which is stored in the
+`personal_updated` attribute. Then the `personal_data_expired`
+property returns ``True`` and the student is redirected to the
+personal data edit page when logging in.
+
+`IStudentNavigation`
+--------------------
+
+See docstring.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentNavigation
+
+Student Study Course Interfaces
+===============================
+
+All data related to an individual course of study are stored with
+the ``studycourse`` object. This container object is an instance of
+the `StudentStudyCourse` class which implements
+`waeup.kofa.students.interfaces.IStudentStudyCourse`,
+`waeup.kofa.students.interfaces.IStudentNavigation` and
+`waeup.kofa.students.interfaces.IStudentStudyCourseTranscript`. The
+latter does not add further behaviour to our classes but is needed
+for transcript pages only. It is not described in the user handbook.
+
+`IStudentStudyCourse`
+---------------------
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentStudyCourse
+
+All attributes are read-only property attributes which are computed
+dynamically.
+
+The first schema field is a `Choice` field which allows values from
+the `CertificateSource`. The source provides all certificates stored
+in the portal. This way, `StudentStudyCourse` objects point to
+exactly one `Certificate` object in the portal. If the certificate
+is removed, an event handler takes care of clearing also all
+referring `certificate` attribute. The tokens of the
+`CertificateSource` are the certicate codes which means, that in
+forms and import files the unique code must be given and the desired
+`Certificate` object will be stored.
+
+The interface has two entry parameter fields. `entry_mode` stores
+the study mode of the programme and `entry_session` the academic
+year when the study course was started. Usually these parameters are
+fixed and must not be changed during course of study.
+
+Very important - or even most important - are the parameters which
+describe the current state of the student's study course.
+`current_session`, `current_level`, `current_verdict`,
+`previous_verdict` and the student's workflow state (see below)
+declare precisely in which academic session and how successfully the
+student has reached the current study level and state in the portal.
+All these attributes are set automatically by the portal und must
+(normally) not be changed via manage form pages or import.
+
+What is a verdict? A verdict is the individual judgement of a jury
+(senate in Nigeria) at the end of each academic session. The jury's
+verdict tells the portal, if the student has successfully completed
+an academic session or not. Depending on the verdict, the student
+will be either allowed to proceed to the next study level or forced
+to repeat the current level. Without a verdict, the student gets
+stuck and cannot even pay school fees for the next academic year.
+This will be further exlplained in the workflow section below.
+
+`StudentStudyCourse` is a container class. Study courses contain
+`StudentStudyLevel` instances which implement
+`waeup.kofa.students.interfaces.IStudentStudyLevel` and also
+`waeup.kofa.students.interfaces.IStudentNavigation`, see above.
+
+`IStudentStudyLevel`
+--------------------
+
+`StudentStudyLevel` instances contain information about the study
+progress achieved at that level. In Kofa study levels range from 100
+to 900. There are two additional levels: a pre-studies level (10)
+and a special postgraduate level (999). There are also two probation
+levels per regular study level. Level 120, for instance, means level
+100 on second probation. See `complete numbering of study levels
+<https://kofa-demo.waeup.org/sources#collapseStudyLevels>`_ in the
+base package.
+
+`StudentStudyLevel` instances are container objects which contain
+course tickets. We therefore sometimes speak of a level course list
+instead of a study level. The study level stores and provides the
+information when (`validation_date`) and by whom (`validated_by`)
+the course list was validated, in which session the level was taken
+(`level_session`) and which verdict the student finally obtained.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentStudyLevel
+
+`StudentStudyLevel` instances also compute some statistics on the
+fly to give an overview of the courses taken. These read-only
+property attributes are: `number_of_tickets`, `passed_params`,
+`gpa_params`, `gpa_params_rectified`, `cumulative_params`,
+`total_credits` and `gpa`. The attentive reader may wonder why the
+latter two are not listed in the attributes section of the interface,
+but as read-only schema fields further below. This is a trick which
+we used to properly display these fields on some display pages and
+pdf slips. However, the `attrs_to_fields` function explicitly
+excludes them when starting the portal, so that these fields are not
+used for persistent storage of same-named attributes in the database.
+
+The property attribute `course_registration_allowed` determines
+whether course registration has ended. In the base package students
+can proceed if they have paid late registration fee. The conditions
+for course registration can be configured in custom packages.
+
+`ICourseTicket`
+---------------
+
+Course tickets allow students to attend a course. Lecturers can
+enter scores at the end of the term. A `CourseTicket` instance
+contains a copy of the original `Course` and `CertificateCourse`
+data. If the courses and/or the referring certificate courses are
+removed, the corresponding tickets remain unchanged. So we do not
+need any event triggered actions on course tickets.
+
+The `CourseTicket` implements
+`waeup.kofa.students.interfaces.ICourseTicket` and also
+`waeup.kofa.students.interfaces.IStudentNavigation`, see above.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: ICourseTicket
+
+The quite long list of schema fields pretends that form pages may
+provide these fields for editing. This is not the case. Except for
+`score`, `mandatory`, `outstanding` and `course_category` (not
+available in base package) all fields are 'for display' only.
+They cannot be changed through the UI. They are solely meant for
+backing up the orginal course data. However, some of them can be
+overwritten by batch processing (import), see
+:ref:`course_ticket_processor`.
+
+`ticket_session` can but must not be used for storing the session in
+which the course was taken. Usually, level and session are inherited
+from the parent study level container. Storing the same
+information in course tickets is not necessary, unless the session
+really differs or 'orphaned' course tickets (tickets without level
+assignment) have to be stored in the Level Zero container.
+`ticket_session` can't be edited through the UI, they only changed
+by import.
+
+.. note::
+
+  It may happen that a course has been accidentally misconfigured, for
+  example the number of credits was set too high. The course object
+  can be corrected, but, course tickets, which refer to this course,
+  can not. They are not adjusted automatically. If you want to correct
+  course tickets you have to replace them by new tickets or
+  to repair the data by batch processing.
+
+Student Payment Interfaces
+==========================
+
+`IStudentOnlinePayment`
+-----------------------
+
+`IStudentOnlinePayment` instances are called student payment tickets
+or sometimes only payments. They contain the data which confirm that
+the student has paid a specific fee.
+`waeup.kofa.students.interfaces.IStudentOnlinePayment` inherits from
+`waeup.kofa.payments.interfaces.IOnlinePayment` and has only two
+additional schema fields: `p_current` and `p_level` which are
+student-specific. It also promises three additional methods which
+process student data after successful or approved payment.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IStudentOnlinePayment
+
+Student Accommodation Interfaces
+================================
+
+`IBedTicket`
+------------
+
+A bed ticket confirms that a bed space in a hostel has been
+allocated to the student for the period of an academic session
+(`booking_session`). The `bed_coordinates` specify which bed space
+has been booked. Sometimes this value is too cryptic and
+inappropriate to guide the student to the bed space. The
+`display_coordinates` property can be used to 'translate' the
+coordinates and display a customary description of the bed space.
+Much like some study level attributes, also `display_coordinates` is
+defined as a read-only schema field in order to automatically
+display the field on form pages. It is excluded by the
+`attrs_to_fields` function and thus not stored in the database.
+
+.. literalinclude:: ../../../../src/waeup/kofa/students/interfaces.py
+   :pyobject: IBedTicket
+
+.. _zope schema: http://docs.zope.org/zope.schema
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/workflow.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/workflow.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/students/workflow.rst	(revision 16045)
@@ -0,0 +1,206 @@
+.. _registration_workflow:
+
+Registration Workflow
+=====================
+
+Studying at a higher education institution means following
+orchestrated and repeatable patterns of activities or, in other
+words, following a process workflow prescribed by the school
+regulations. This process starts with application for studying a
+programme offered by the school, and may end with de-registration of
+a student. But even after de-registration, former students can be
+kept in the system as alumni so that they can still access their
+records or can apply for official transcripts.
+
+Kofa divides these 'repeatable patterns of activities' into two
+different and strictly seperated process workflows: an application
+and a student registration workflow. The latter, which presupposes
+the admission of the student, will be described here.
+
+A worflow defines states and transitions between the states. The
+following diagram shows the entire student registration workflow.
+Workflow states are connected with dashed lines which symbolize
+workflow transitions. There are no arrows at the end of each line
+because in most cases, Kofa provides transitions in both directions.
+Transitions are denoted by lower-case characters, reverse
+transitions additionally by a preceded dash. A few transitions do
+not make sense and are thus not available (N/A)::
+
+              a
+    created ------- admitted
+               b       |
+      +----------------+
+      |                         e
+      +------------------------------------------------------+
+      |                  c                            d      |
+    clearance started ------- clearance requested  ------- cleared
+                                                             |
+                                 f                           |
+      +--------+---------------------------------------------+
+      |        |
+      | h    school fee paid -------- returning -------------+
+      |        |             \   l        |                  |
+      +--------+ g            \ _ _ _ m   | k                |
+               |                      \   |                  |
+            courses registered ------ courses validated      |
+                           |      i       |                  |
+                           |              | n                |
+                           | j            |                  |
+                           |      +-- graduated              |
+                           |      |       |                  |
+                           |      |       | o                |
+                           |      |       |                  |
+                           |      |   transcript requested   |
+     a: admit              |      |       |                  |
+    -a: reset1             |    r |       | p                |
+     b: start_clearance    |      |       |                  |
+    -b: reset2             |      |   transcript validated   |
+     c: request_clearance  |      |       |                  |
+    -c: reset3             |      |       | q                |
+        (= reject request) |      |       |                  |
+     d: clear              |      +-- transcript released    |
+    -d: N/A                +---------------------------------+
+     e: N/A
+    -e: reset4 (= annul clearance)
+     f: pay_first_school_fee, approve_first_school_fee
+    -f: reset5 (= annul payment)
+     g: register_courses
+    -g: reset7 (= unregister)
+     h: pay_pg_fee, approve_pg_fee
+    -h: N/A
+     i: validate_courses
+    -i: N/A
+     j: bypass_validation
+    -j: N/A
+     k: return
+    -k: reset9
+     l: pay_school_fee, approve_school_fee
+    -l: reset6 (= annul payment)
+     m: N/A
+    -m: reset8 (= annul validation)
+     n: N/A
+    -n: N/A
+     o: request_transcript
+    -o: reset10 (= reject request)
+     p: validate_transcript
+    -p: N/A
+     q: release_transcript
+    -q: N/A
+     r: N/A
+    -r: reset11
+
+Student registration starts with the first login of the student
+into state ``admitted``. After editing the email address and phone
+number fields, and also uploading a passport picture (subject to
+customization), the student immediately proceeds with the clearance
+sub-workflow. S/he starts clearance **(b)** either directly or by
+using a clearance access code, depending on the configuration of
+the portal. After filling the clearance form and uploading all
+necessary documents, the student requests clearance **(c)** which
+can be either accepted **(d)** or rejected **(-c)** by a clearance
+officer. A cleared student can enter the 'cyclical' part of the
+workflow which starts after first school fee payment **(f)**.
+
+A properly registering undergraduate student creates a new study
+level (course list) at the beginning of each academic session, adds
+the necessary course tickets and finally registers the list **(g)**.
+The student's course adviser subsequently validates the list **(i)**,
+or rejects the course registration request **(-g)** so that the
+student has to register again. Even after validation there is an
+option to annul the already validated course list **(-m)** so that
+the student can revise the list. At the end of the session, when all
+exams have been taken, course results (scores) are either entered by
+lecturers or imported by an import officer, a verdict is announced
+and the student is finally enabled to return **(k)**. In Kofa the
+last two steps are automatically performed by the Verdict Batch
+Processor. In some cases, course validators are not available or
+course validation is not necessary. Then validation can be skipped
+**(j)** by setting `bypass_validation` in the processor's import
+file to ``True``. In both cases the student arrives in state
+``returning`` but is still in the same session. Also the current
+level has not changed. The new session starts only if the student
+pays school fee for the subsequent session **(l)**. Then
+`current_session` increases by one, `current_level` is set according
+to the verdict obtained, the old verdict is copied into
+`previous_verdict` and `current_verdict` is cleared. The student has
+arrived in the new academic session in state ``school_fee_paid``.
+The **gikl** cycle is repeatedly passed through till the student is
+ready for graduation.
+
+So far we have spoken about undergraduate students. The same
+sequence of transitions also applies to postgraduate students if
+they have to pass several levels (e.g. 700 and 800). Very often the
+level model does not apply to postgraduates. The students remain in
+the same (999) level but pay for each year of studying. In this case
+the **gikl** cycle collapses to the **h** cycle. Students may add
+course tickets whenever they like, but cannot register their course
+list.
+
+After graduation, former students (alumni) can apply for an official
+transcript. The transcript processing workflow (**opqr**) is a
+subset of the registration workflow which starts in state
+``graduated``. Many people are involved in transcript processing.
+Therefore, Kofa provides two additional roles, the Transcript
+Officer and Transcript Signee roles. Both are available globally and
+locally (at faculty level). First of all, the graduated student has
+to request a transcript **(o)** by filling a request form. The
+Transcript Officer will see the new request and can either reject
+the request **(-o)** or validate the request **(p)**. Before
+validation course results can be edited or added by a Students
+Manager, transcript study level remarks also by Transcript Officers.
+After validation the entire studycourse including its content is
+locked. Nobody will be able to modify the course lists, neither
+through the UI nor by import. Transcripts can now be electronically
+signed by Transcript Signees. Once all signees have done their job,
+the Transcript Officer can release the transcript **(q)**. A pdf
+file is created and stored in the file system. The transcript pdf
+slip contains all signatures. The dynamic transcript generation is
+disabled. Nothing can be changed now, unless the entire transcript
+process is reset **(-r)** by a Students Manager. Then comments and
+signatures will be removed and the pdf file deleted. The student can
+start afresh.
+
+.. _registration_workflow_batch_processing:
+
+Workflow Batch Processing
+=========================
+
+The :py:class:`StudentProcessor
+<waeup.kofa.students.batching.StudentProcessor>` allows to import
+either workflow states or transitions. As already emphasized in the
+description of the processor class, we refer to them as unsafe and
+safe respectively. Transitions are only possible between allowed
+workflow states. Only transitions ensure that the registration
+workflow is maintained. Setting the workflow state by import is
+considered brute and must be avoided.
+
+
+.. _student_history:
+
+Student History
+===============
+
+All transitions are automatically logged in ``students.log``. And
+also the import of workflow states is recorded in the logfile.
+However, these logfiles can only be accessed by some officers and
+are hidden from students. Since Kofa takes up the cause of
+transparancy, we are of the opinion, that also students must know,
+when and by whom the state of their record was changed. Therefore we
+store all workflow-relevant changes additionally in the student
+history which is attached to the student object. The history is a
+list of messages. Each message contains the local time, the workflow
+transition message and the public name of the user who triggered the
+transition, either online or by import::
+
+  2015-05-16 05:11:34 UTC - Record created by System Admin
+  2015-05-30 07:34:09 UTC - Admitted by System Admin
+  2015-05-30 08:34:11 UTC - Clearance started by John Doe
+  2015-05-30 09:34:15 UTC - Clearance requested by John Doe
+  2015-05-30 10:37:27 UTC - Cleared by Clearance Officer
+
+If the workflow state is set by import, the following message would
+have been added instead::
+
+  2015-05-30 10:37:27 UTC - State 'cleared' set by Import Officer
+
+Student histories are exportable but cannot be imported.
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing.rst	(revision 16045)
@@ -0,0 +1,251 @@
+.. _testing:
+
+Testing
+*******
+
+Introduction
+============
+
+Kofa's Python code is being tested automatically. The developers'
+goal is to reach 100% code coverage by Kofa's test runners, which
+means that each single line of code is passed at least one time when
+running the tests.
+
+Why testing? Testing makes sure that our code works properly under
+given sets of conditions and, only comprehensive testing ensures
+that changing or customizing the code does not break existing
+functionality.
+
+Why *automated* testing? Simply because no developer likes to click
+around the user interface to check tons of functionality. In Kofa
+more than 1300 tests, with dozens of actions per test, are being
+executed each time when the testrunner is started. This job can't be
+done manually.
+
+What we test: "Unit" and "Functional" Tests
+-------------------------------------------
+
+There are two different ways to test the code of a web application
+automatically: *unit tests* and *functional tests*. The goal of unit
+testing is to isolate each part of the program and show that the
+individual parts are correct. Functional tests of a web application
+are more wholistic, they require a running web application in the
+background with even a working (temporary) database. Functional
+tests are typically linked to a particular use case, they are
+interacting with the portal as a user would. That implies that
+functional testing has to make use of a test browser. A test browser
+does the same job as normal web browser does, except for visualizing
+the rendered HTML code.
+
+
+How we test: "Python" and "Doctest" Tests
+-----------------------------------------
+
+There are also two different ways to integrate tests, either
+functional or unit, into a Python package: *Doctest tests* (or doctests)
+and *Python tests*. Python test modules are a collection of
+isolatable Python test cases. A test case combines a collection of
+test methods which are being executed by the testrunner one
+after the other. Python test modules are automatically identified by
+means of their filenames which start with ``test_``. In contrast,
+doctests can be wrapped up in simple text files (ending with
+``.txt``), or they can be put into docstrings of the application's
+source code itself. Common to all doctests is that they are based on
+output from the standard Python interpreter shell (indicated by the
+interpreter prompt ``>>>``. The doctest runner parses all ``py`` and
+``txt`` files in our package, executes the interpreter commands
+found and compares the output against an expected value. Example::
+
+  Python's `math` module can compute the square root of 2:
+
+  >>> from math import sqrt
+  >>> sqrt(2)
+  1.4142135623730951
+
+Why wrapping tests into documentation? An objective of testdriven
+software development is also the documentation of the 'Application
+Programming Interface' (API) and, to a certain extent, providing a
+guideline to users, how to operate the portal. The first is mainly
+done in the docstrings of Python modules which present an expressive
+documentation of the main use cases of a module and its components.
+The latter is primarily done in separate ``txt`` files.
+
+When starting the development of Kofa, we relied on writing a
+coherent documentation including doctests in restructured text
+format. During the software development process, the focus shifted
+from doctesting to pure Python testing with a lot of functional
+tests inside. It turned out that Python tests are easier to handle
+and more powerful. Drawback is, that these tests cannot easily be
+integrated into the Sphinx documentation project (the documentation
+which you are reading right now). However, we will list some of
+these tests and try to explain what they are good for.
+
+
+Doctest Tests
+=============
+
+Browsing
+--------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/pages
+   testing/breadcrumbs
+
+Cataloging
+----------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/catalog
+
+Data Center
+-----------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/datacenter
+
+Security
+--------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/permissions
+
+Officers
+--------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/userscontainer
+
+University
+----------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/app
+
+Academic Section
+----------------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/certcourse
+
+Batch Processing
+----------------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/batchprocessing
+   testing/batchprocessing_browser
+
+Access Codes
+------------
+
+.. toctree::
+   :maxdepth: 2
+
+   testing/accesscode
+
+
+Python Tests
+============
+
+There are hundreds of Python test cases in Kofa with many test
+methods each. Here we present only a few of them. The test methods
+are easy to read. In most cases they are functional and certain
+methods and properties of a test browser are called. Most important
+are `browser.open()` (opens a web page), `browser.getControl()`
+(gets a control button), `browser.getLink()` (gets a link) and
+`browser.contents` (is the HTML content of the opened page).
+
+
+.. _test_suspended_officer:
+
+Suspended Officer Account
+-------------------------
+
+The first test can be found in
+`waeup.kofa.browser.tests.test_browser.SupplementaryBrowserTests`.
+The test makes sure that suspended officers can't login but see a
+proper warning message when trying to login. Furthermore, suspended
+officer accounts are clearly marked and a warning message shows up
+if a manager accesses a suspended account, see :ref:`Officers
+<officers>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/browser/tests/test_browser.py
+   :pyobject: SupplementaryBrowserTests.test_suspended_officer
+
+
+.. _test_handle_clearance:
+
+Handling Clearance by Clearance Officer
+---------------------------------------
+
+This test can be found in
+`waeup.kofa.students.tests.test_browser.OfficerUITests`. The corresponding use
+case is partly described :ref:`elsewhere <rejecting_clearance>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/students/tests/test_browser.py
+   :pyobject: OfficerUITests.test_handle_clearance_by_co
+
+
+.. _test_handle_courses:
+
+Handling Course List Validation by Course Adviser
+-------------------------------------------------
+
+This test can be found in
+`waeup.kofa.students.tests.test_browser.OfficerUITests`. The corresponding use
+case is described :ref:`elsewhere <course_registration>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/students/tests/test_browser.py
+   :pyobject: OfficerUITests.test_handle_courses_by_ca
+
+
+.. _test_batch_editing_scores:
+
+Batch Editing Scores by Lecturers
+---------------------------------
+
+These test can be found in
+`waeup.kofa.students.tests.test_browser.LecturerUITests`. The corresponding use
+cases are described :ref:`elsewhere <batch_editing_scores>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/students/tests/test_browser.py
+   :pyobject: LecturerUITests
+
+.. _test_manage_hostels:
+
+Manage Hostel
+-------------
+
+This test can be found in
+`waeup.kofa.hostels.tests.HostelsUITests`. The corresponding use
+case is described :ref:`elsewhere <hostels_pages>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/hostels/tests.py
+   :pyobject: HostelsUITests.test_add_search_edit_delete_manage_hostels
+
+.. _test_handle_accommodation:
+
+Bed Space Booking
+-----------------
+
+This test can be found in
+`waeup.kofa.students.tests.test_browser.StudentUITests`. The corresponding use
+case is described :ref:`elsewhere <bed_tickets>`.
+
+.. literalinclude:: ../../../src/waeup/kofa/students/tests/test_browser.py
+   :pyobject: StudentUITests.test_student_accommodation
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/accesscode.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/accesscode.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/accesscode.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _accesscode_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/accesscode_browser.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/app.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/app.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/app.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _app_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/app.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _batchprocessing_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/batchprocessing.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing_browser.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing_browser.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/batchprocessing_browser.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _batchprocessing_browser_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/batchprocessing_browser.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/breadcrumbs.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/breadcrumbs.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/breadcrumbs.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _breadcrumbs_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/breadcrumbs.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/catalog.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/catalog.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/catalog.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _catalog_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/catalog.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/certcourse.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/certcourse.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/certcourse.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _certcourse_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/certcourse.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/datacenter.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/datacenter.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/datacenter.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _datacenter_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/datacenter.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/pages.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/pages.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/pages.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _pages_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/pages.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/permissions.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/permissions.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/permissions.rst	(revision 16045)
@@ -0,0 +1,5 @@
+.. _security_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/permissions.txt
+
+.. include:: ../../../../src/waeup/kofa/doctests/authentication.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/userscontainer.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/userscontainer.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/testing/userscontainer.rst	(revision 16045)
@@ -0,0 +1,3 @@
+.. _userscontainer_txt:
+
+.. include:: ../../../../src/waeup/kofa/doctests/userscontainer.txt
Index: main/waeup.kofa/branches/uli-py3/docs/source/userdocs/users.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/docs/source/userdocs/users.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/docs/source/userdocs/users.rst	(revision 16045)
@@ -0,0 +1,95 @@
+.. _users:
+
+Users
+*****
+
+Kofa distinguishes between four types of users: anonymous users,
+applicants, students, portal officers/managers and the Zope manager.
+Kofa distinguishes further between authenticated and unauthenticated
+users and between users with and without account object.
+
+The user authentication process in Kofa is quite complex. Briefly
+and in simple terms, a so-called authenticator (plugin) validates
+the credentials provided via the login form, fetches the matching
+account object, extracts the account data and finally leads to the
+creation of a temporary user object, a so-called principal. Kofa
+principals provide `name`, `title`, `email`, `phone`, `public_name`
+and `user_type` attributes.
+
+
+Anonymous
+=========
+
+Anonymous users do not authenticate and thus do not provide
+credentials. Their user id in logfiles is always ``zope.anybody``.
+These users gain two permissions: :py:class:`Anonymous
+<waeup.kofa.permissions.Anonymous>` and :py:class:`Public
+<waeup.kofa.permissions.Public>`.
+
+Only a few actions of anonymous users are logged. These are payment
+data webservice requests, the execution of password mandates and the
+self-registration of applicants.
+
+.. _applicants_and_students:
+
+Applicants and Students
+=======================
+
+Logged-in applicants or students are regular authenticated Kofa
+users. Their user account object is an adaption of their
+applicant/student object. More precisely, the Applicant/Student
+authenticator fetches the matching applicant/student object and
+turns it on-the-fly into a Kofa account object which is further used
+for creating a principal instance. The `applicant_id`/`student_id`
+attribute becomes the user name and the `display_fullname` property
+serves as user title.
+
+.. _officers:
+
+Officers
+========
+
+.. seealso::
+
+   :ref:`Officers Doctests <userscontainer_txt>`
+
+   :ref:`Officers Python Test <test_suspended_officer>`
+
+Officers are users with a dedicated user account object stored in the
+``users`` container (of type :py:class:`UsersContainer
+<waeup.kofa.userscontainer.UsersContainer>`) which is located in Kofa's root
+container. The account object has three more attributes than the principal
+instance which is created from the account data: (1) a `suspended` attribute
+which allows to deactivate an account, (2) a `roles` attribute which is a list
+of global role names assigned to the officer, and (3) a private `_local_roles`
+attribute which maps local role names to lists of objects the respective local
+role applies to. This information is important because local role assignment
+is originally stored only with the objects the role applies to and not with
+the user who got the role. When removing an officer, Kofa iterates over the
+mapping and the list of objects in order to remove all these local role
+assignments denoted in the mapping.
+
+The management of portal officers is done in the 'Officers' section
+of Kofa. The management page shows all officers registered in the
+portal together with their global and local roles. The table can be
+easily sorted or filtered. Deactivated officer accounst are marked
+``(suspended)``.
+
+
+Manager
+=======
+
+There is one and only one manager account (user id ``zope.manager``)
+in Kofa. The manager has access to the root instance of the Kofa
+application which has its own user interface (see screenshot below).
+Through this 'Grok UI' the manager can access some basic functions
+to manage the database and also access the Kofa user interface.
+
+.. image:: images/Grok_UI.png
+
+Although the manager automatically gains all permissions the system
+defines, this real superuser (or emergency user) neither has an
+account in Kofa nor can access Kofa through its regular login page.
+The manager has to enter the portal through the Grok UI which
+usually ony runs on a localhost port, for example
+``http://localhost:8080``.
Index: main/waeup.kofa/branches/uli-py3/etc/README.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/README.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/README.txt	(revision 16045)
@@ -0,0 +1,26 @@
+In this directory you can find templates which are used by
+``zc.buildout`` to create the configuration files in the parts/etc/ subdir
+of your project.
+
+If you modify files in this directory, you have to run::
+
+  $ bin/buildout
+
+afterwards to rebuild the configuration files in parts/etc/.
+
+In the templates you can use placesholders recognized by zc.buildout
+to name local paths, etc. A zc.buildout placeholder looks like this::
+
+  ${buildout:directory}
+
+which gives you the path of the project directory and will be
+substituted with the real path when you run buildout the next
+time. The set of available placeholders depends on your
+buildout.cfg.
+
+You can also modify files in parts/etc directly, but those changes
+will be overwritten after running bin/buildout the next time.
+
+To run your project you can do::
+
+  $ bin/paster serve parts/etc/deploy.ini
Index: main/waeup.kofa/branches/uli-py3/etc/profile.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/profile.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/profile.ini.in	(revision 16045)
@@ -0,0 +1,93 @@
+# profile.ini
+#
+# Profiling configuration for use with paster/WSGI
+#
+# Call http://localhost:8080/__profile__ to see the profiler in action
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:profiler]
+use = egg:repoze.profile#profile
+
+[app:app]
+use = egg:${kofa_params:devel_pkg}
+
+[pipeline:main]
+pipeline = translogger profiler app
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/waeup.kofa/branches/uli-py3/etc/raw/debug.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/raw/debug.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/raw/debug.ini.in	(revision 16045)
@@ -0,0 +1,94 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter-app:main]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+next = zope
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}#debug
+filter-with = translogger
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/waeup.kofa/branches/uli-py3/etc/raw/deploy.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/raw/deploy.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/raw/deploy.ini.in	(revision 16045)
@@ -0,0 +1,94 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:main]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
Index: main/waeup.kofa/branches/uli-py3/etc/site.zcml.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/site.zcml.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/site.zcml.in	(revision 16045)
@@ -0,0 +1,82 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:kofa="http://namespaces.waeup.org/kofa"
+           i18n_domain="${kofa_params:devel_pkg}">
+
+  <include package="${kofa_params:devel_pkg}" />
+  <include package="${kofa_params:devel_pkg}" file="mail.zcml" />
+  <!-- install job container
+
+       install a job container for asynchronous jobs. Pick one of the
+       two possibilites: 'single' for installation in the one big ZODB
+       or 'multidb' for setup with a dedicated ZODB for job handling.
+  -->
+  <!-- include package="waeup.kofa" file="async_single.zcml" / -->
+  <include package="waeup.kofa" file="async_multidb.zcml" />
+
+  <!-- install dispatcher and other needed async components
+
+       install other needed components for asynchronous jobs. Pick one
+       of the two possibilities: 'basic_dispatcher' for single ZODB
+       setup, 'multidb_dispatcher' for setup with a dedicated ZODB for
+       job handling. Make sure the setting matches the setting picked
+       above. -->
+  <!-- include package="zc.async" file="basic_dispatcher_policy.zcml" / -->
+  <include package="zc.async" file="multidb_dispatcher_policy.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <kofa:datacenter
+      path="${buildout:directory}/var/datacenter" />
+
+    <configure i18n_domain="${kofa_params:devel_pkg}">
+
+
+      <unauthenticatedPrincipal id="zope.anybody"
+                                title="Unauthenticated User" />
+      <unauthenticatedGroup id="zope.Anybody"
+                            title="Unauthenticated Users" />
+      <authenticatedGroup id="zope.Authenticated"
+                        title="Authenticated Users" />
+      <everybodyGroup id="zope.Everybody"
+                      title="All Users" />
+      <principal id="zope.manager"
+                 title="Manager"
+                 login="grok"
+                 password_manager="SHA1"
+                 password="321b9cd3739bd029629243732eb682db005a7541f7509622"
+                 />
+
+      <!-- XMLRPC user for demo and testing
+           Plaintext password is xmlrpcuser1
+           Don't use it in real deployment -->
+      <principal id="zope.xmlrpcuser1"
+                 title="XMLRPC User 1"
+                 login="xmlrpcuser1"
+                 password="{SSHA}ZaL4-UiwmSu6AbLYY-yCS_8vtKpH5Hc6"
+                 password_manager="SSHA"
+                 />
+
+      <grant role="waeup.xmlrpcusers1"
+             principal="zope.xmlrpcuser1" />
+
+
+      <!-- Replace the following directive if you do not want
+           public access -->
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Anybody" />
+      <grant permission="zope.View"
+             principal="zope.Authenticated" />
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Authenticated" />
+      <grant permission="waeup.Public"
+             principal="zope.Everybody" />
+      <grant permission="waeup.Anonymous"
+             principal="zope.Anybody" />
+      <grant permission="waeup.Authenticated"
+             principal="zope.Authenticated" />
+
+      <role id="zope.Manager" title="Site Manager" />
+      <grantAll role="zope.Manager" />
+      <grant role="zope.Manager"
+             principal="zope.manager" />
+   </configure>
+</configure>
Index: main/waeup.kofa/branches/uli-py3/etc/themed/debug.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/themed/debug.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/themed/debug.ini.in	(revision 16045)
@@ -0,0 +1,118 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI and diazo
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[filter:debugger]
+# Change the last part from 'ajax' to 'pdb' for a post-mortem debugger
+# on the console:
+use = egg:z3c.evalexception#ajax
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}#debug
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to false, if you don't want rules.xml being rebuild on each request
+debug = true
+
+[pipeline:default]
+pipeline = theme
+           translogger
+           debugger
+           zope
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/waeup.kofa/branches/uli-py3/etc/themed/deploy.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/themed/deploy.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/themed/deploy.ini.in	(revision 16045)
@@ -0,0 +1,118 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'access.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:content]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static/
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:base_port}
+
+[filter:theme]
+use = egg:diazo
+rules = file:///%(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[pipeline:default]
+pipeline = theme
+           content
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+# the directory where all real diazo config is done (no trailing slash)
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/waeup.kofa/branches/uli-py3/etc/themed/zeo1.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/themed/zeo1.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/themed/zeo1.ini.in	(revision 16045)
@@ -0,0 +1,119 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static/
+
+[pipeline:default]
+pipeline = theme
+           zope
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = %(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:zeo1_port}
+threadpool_workers = ${kofa_params:threadpool_workers}
+use_threadpool = True
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope_zeo1.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/waeup.kofa/branches/uli-py3/etc/themed/zeo2.ini.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/themed/zeo2.ini.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/themed/zeo2.ini.in	(revision 16045)
@@ -0,0 +1,119 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi, async, async_trace
+
+[handlers]
+keys = null, console, accesslog, asynclog, async_tracelog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+#handlers = accesslog
+handlers = null
+qualname = wsgi
+propagate = 0
+
+[logger_async]
+level = INFO
+handlers = asynclog
+qualname = zc.async
+propagate = 0
+
+[logger_async_trace]
+level = INFO
+handlers = async_tracelog
+qualname = zc.async.trace
+propagate = 0
+
+[handler_null]
+# you can use 'NullHandler' (w/o the path) if under Python 2.7+
+class = waeup.kofa.utils.helpers.NullHandler
+args = ()
+level = NOTSET
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[handler_accesslog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2.log'),
+        'a')
+level = INFO
+formatter = accesslog
+
+[handler_asynclog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[handler_async_tracelog]
+class = FileHandler
+args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async_trace.log'),
+        'a')
+level = INFO
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[filter:translogger]
+use = egg:Paste#translogger
+setup_console_handler = False
+logger_name = wsgi
+
+[app:zope]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[app:static]
+use = egg:Paste#static
+document_root = %(LAYOUT_DIR)s/static
+
+[pipeline:default]
+pipeline = theme
+           zope
+
+[composite:main]
+use = egg:Paste#urlmap
+/static = static
+/ = default
+
+[filter:theme]
+use = egg:diazo
+rules = %(LAYOUT_DIR)s/rules.xml
+prefix = /
+doctype = <!DOCTYPE html>
+# set to true, if you want rules.xml being rebuild on each request
+debug = false
+
+[server:main]
+use = egg:Paste#http
+host = ${kofa_params:host}
+port = ${kofa_params:zeo2_port}
+threadpool_workers = ${kofa_params:threadpool_workers}
+use_threadpool = True
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope_zeo2.conf
+env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid2.txt
+LAYOUT_DIR = ${buildout:directory}/layout
+STATIC_DIR = %(LAYOUT_DIR)s/static
Index: main/waeup.kofa/branches/uli-py3/etc/zdaemon.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zdaemon.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zdaemon.conf.in	(revision 16045)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve ${buildout:directory}/parts/etc/themed-deploy.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon.log
+  socket-name ${zope_conf:logfiles}/zdaemonsock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: main/waeup.kofa/branches/uli-py3/etc/zeo1.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zeo1.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zeo1.conf.in	(revision 16045)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo1.pid ${buildout:directory}/parts/etc/themed-zeo1.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon-zeo1.log
+  socket-name ${zope_conf:logfiles}/zeo1.sock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: main/waeup.kofa/branches/uli-py3/etc/zeo2.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zeo2.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zeo2.conf.in	(revision 16045)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo2.pid ${buildout:directory}/parts/etc/themed-zeo2.ini
+  daemon on
+  transcript ${zope_conf:logfiles}/zdaemon-zeo2.log
+  socket-name ${zope_conf:logfiles}/zeo2.sock
+  # Enable this to run the child process as a different user
+  # user zope
+</runner>
Index: main/waeup.kofa/branches/uli-py3/etc/zope.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zope.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zope.conf.in	(revision 16045)
@@ -0,0 +1,62 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+  # Standard blob storage
+  <blobstorage>
+    blob-dir ${zope_conf:blobstorage}
+    <filestorage>
+      path ${zope_conf:filestorage}/Data.fs
+    </filestorage>
+  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+#  <zeoclient>
+#    server localhost:8100
+#    storage 1
+#    # ZEO client cache, in bytes
+#    cache-size 20MB
+#    # Uncomment to have a persistent disk cache
+#    #client zeo1
+#  </zeoclient>
+
+</zodb>
+
+<zodb async>
+  <filestorage>
+    # create true
+    path ${zope_conf:filestorage}/Data.async.fs
+  </filestorage>
+</zodb>
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+# devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: main/waeup.kofa/branches/uli-py3/etc/zope_zeo1.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zope_zeo1.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zope_zeo1.conf.in	(revision 16045)
@@ -0,0 +1,100 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+
+  #cache-size
+
+  # num of concurrent pre-opened ZODB threads; default: 7
+  pool-size 25
+
+  # Standard blob storage
+#  <blobstorage>
+#    blob-dir ${zope_conf:blobstorage}
+#    <filestorage>
+#      path ${zope_conf:filestorage}/Data.fs
+#    </filestorage>
+#  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+  <zeoclient>
+    server localhost:8100
+    storage 1
+     # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_1:filestorage}/zeo1
+  </zeoclient>
+
+# Uncomment this if you want to run a ZEO server instead:
+#  <zeo>
+#    # required; 'host:port' for TCP/IP, path to a socket file for UDP
+#    address localhost:8090
+#
+#    # optional; 'address' remarks apply
+#    #monitor-address localhost:8091
+#
+#    # optional; keep queue of last N transactions. Speeds up client cache verif.
+#    #invalidation-queue-size 10
+#
+#    # optional; is this a read-only instance? true or false
+#    #read-only false
+#
+#    # optional; seconds to wait for client to commit after acquiring lock
+#    #transaction-timeout 20
+#
+#    # optional; authentication protocol. With ZEO only 'digest' supported
+#    #authentication-protocol digest 
+#
+#    # optional; Path of the database containing authentication credentials.
+#    #authentication-database
+#
+#    # optional; the authentication realm of server
+#    #authentication-realm myrealm
+#  </zeo>
+
+</zodb>
+
+<zodb async>
+  <zeoclient>
+    server localhost:8100
+    storage async
+    name async
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_1:filestorage}/zeo1_async
+  </zeoclient>
+</zodb>
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/zeo1_z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+#devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: main/waeup.kofa/branches/uli-py3/etc/zope_zeo2.conf.in
===================================================================
--- main/waeup.kofa/branches/uli-py3/etc/zope_zeo2.conf.in	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/etc/zope_zeo2.conf.in	(revision 16045)
@@ -0,0 +1,101 @@
+# Identify the component configuration used to define the site:
+site-definition ${site_zcml:output}
+
+<zodb>
+
+  #cache-size
+
+  # num of concurrent pre-opened ZODB threads; default: 7
+  pool-size 7
+
+  # Standard blob storage
+#  <blobstorage>
+#    blob-dir ${zope_conf:blobstorage}
+#    <filestorage>
+#      path ${zope_conf:filestorage}/Data.fs
+#    </filestorage>
+#  </blobstorage>
+
+# Uncomment this if you want a blob-less standard file storage instead:
+#  <filestorage>
+#       path ${zope_conf:filestorage}
+#  </filestorage>
+
+# Uncomment this if you want to connect to a ZEO server instead:
+  <zeoclient>
+    server localhost:8100
+    storage 1
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_2:filestorage}/zeo2
+  </zeoclient>
+
+# Uncomment this if you want to run a ZEO server instead:
+#  <zeo>
+#    # required; 'host:port' for TCP/IP, path to a socket file for UDP
+#    address localhost:8090
+#
+#    # optional; 'address' remarks apply
+#    #monitor-address localhost:8091
+#
+#    # optional; keep queue of last N transactions. Speeds up client cache verif.
+#    #invalidation-queue-size 10
+#
+#    # optional; is this a read-only instance? true or false
+#    #read-only false
+#
+#    # optional; seconds to wait for client to commit after acquiring lock
+#    #transaction-timeout 20
+#
+#    # optional; authentication protocol. With ZEO only 'digest' supported
+#    #authentication-protocol digest 
+#
+#    # optional; Path of the database containing authentication credentials.
+#    #authentication-database
+#
+#    # optional; the authentication realm of server
+#    #authentication-realm myrealm
+#  </zeo>
+
+</zodb>
+
+<zodb async>
+  <zeoclient>
+    server localhost:8100
+    storage async
+    name async
+    # ZEO client cache, in bytes
+    cache-size 20MB
+    # Uncomment to have a persistent disk cache
+    client ${zope_conf_zeo_2:filestorage}/zeo2_async
+  </zeoclient>
+</zodb>
+
+
+<eventlog>
+  # This sets up logging to a file.
+  # The "path" setting can be a relative or absolute
+  # filesystem path.
+
+  <logfile>
+    path ${zope_conf:logfiles}/zeo2_z3.log
+    formatter zope.exceptions.log.Formatter
+  </logfile>
+
+  # This sets up logging to to standard output.
+  # The "path" setting can be the tokens STDOUT or STDERR
+  
+#  <logfile>
+#    path STDOUT
+#    formatter zope.exceptions.log.Formatter
+#  </logfile>
+</eventlog>
+
+# Comment this line to disable developer mode.  This should be done in
+# production
+#devmode on
+
+# Extra configuration lines can be added to zope_conf's extra option. Put for 
+# instance productconf sections in here.
+${zope_conf:extra}
Index: main/waeup.kofa/branches/uli-py3/grok-1.1.cfg
===================================================================
--- main/waeup.kofa/branches/uli-py3/grok-1.1.cfg	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/grok-1.1.cfg	(revision 16045)
@@ -0,0 +1,192 @@
+[versions]
+# from http://svn.zope.org/*checkout*/zopetoolkit/trunk/zopeapp.cfg?rev=108938
+zope.app.apidoc = 3.7.1
+zope.app.applicationcontrol = 3.5.5
+zope.app.appsetup = 3.13.0
+zope.app.authentication = 3.7.1
+zope.app.basicskin = 3.5.0
+zope.app.broken = 3.5.0
+zope.app.cache = 3.7.0
+zope.app.catalog = 3.8.1
+zope.app.component = 3.8.4
+zope.app.container = 3.8.2
+zope.app.content = 3.4.0
+zope.app.dav = 3.5.2
+zope.app.debug = 3.4.1
+zope.app.dependable = 3.5.1
+zope.app.error = 3.5.2
+zope.app.exception = 3.6.1
+zope.app.file = 3.5.1
+zope.app.folder = 3.5.1
+zope.app.form = 4.0.1
+zope.app.ftp = 3.5.0
+zope.app.generations = 3.5.1
+zope.app.http = 3.6.1
+zope.app.i18n = 3.6.2
+zope.app.interface = 3.5.0
+zope.app.interpreter = 3.4.0
+zope.app.intid = 3.7.1
+zope.app.keyreference = 3.6.0
+zope.app.locales = 3.6.0
+zope.app.localpermission = 3.7.0
+zope.app.locking = 3.5.0
+zope.app.onlinehelp = 3.5.2
+zope.app.pagetemplate = 3.10.1
+zope.app.preference = 3.6.0
+zope.app.preview = 3.4.0
+zope.app.principalannotation = 3.7.0
+zope.app.publication = 3.10.2
+zope.app.publisher = 3.10.1
+zope.app.renderer = 3.5.1
+zope.app.rotterdam = 3.5.1
+zope.app.schema = 3.5.0
+zope.app.security = 3.7.5
+zope.app.securitypolicy = 3.5.2
+zope.app.server = 3.5.0
+zope.app.session = 3.6.1
+zope.app.skins = 3.4.0
+zope.app.testing = 3.7.4
+zope.app.tree = 3.6.0
+zope.app.twisted = 3.5.0
+zope.app.undo = 3.5.0
+zope.app.wsgi = 3.6.1
+zope.app.zcmlfiles = 3.7.0
+zope.app.zopeappgenerations = 3.5.0
+zope.app.zptpage = 3.5.1
+roman = 1.4.0
+zc.sourcefactory = 0.6.0
+zodbcode = 3.4.0
+zope.thread = 3.4
+zope.rdb = 3.5.0
+
+# from http://svn.zope.org/*checkout*/zopetoolkit/trunk/ztk.cfg?rev=108938
+zope.annotation = 3.5.0
+zope.applicationcontrol = 3.5.5
+zope.authentication = 3.7.0
+zope.broken = 3.6.0
+zope.browser = 1.2
+zope.browsermenu = 3.9.0
+zope.browserpage = 3.11.0
+zope.browserresource = 3.10.2
+zope.cachedescriptors = 3.5.0
+zope.catalog = 3.8.1
+zope.component = 3.9.1
+zope.componentvocabulary = 1.0
+zope.configuration = 3.7.1
+zope.container = 3.11.0
+zope.contentprovider = 3.6.1
+zope.contenttype = 3.5.0
+zope.copy = 3.5.0
+zope.copypastemove = 3.6.0
+zope.datetime = 3.4.0
+zope.deferredimport = 3.5.0
+zope.deprecation = 3.4.0
+zope.documenttemplate = 3.4.2
+zope.dottedname = 3.4.6
+zope.dublincore = 3.6.0
+zope.error = 3.7.0
+zope.event = 3.4.1
+zope.exceptions = 3.5.2
+zope.file = 0.5.0
+zope.filerepresentation = 3.6.0
+zope.formlib = 4.0
+zope.hookable = 3.4.1
+zope.html = 2.0.0
+zope.i18n = 3.7.2
+zope.i18nmessageid = 3.5.0
+zope.index = 3.6.0
+zope.interface = 3.5.3
+zope.intid = 3.7.2
+zope.keyreference = 3.6.2
+zope.lifecycleevent = 3.6.0
+zope.location = 3.9.0
+zope.login = 1.0.0
+zope.mimetype = 1.2.0
+zope.minmax = 1.1.2
+zope.pagetemplate = 3.5.0
+zope.password = 3.5.1
+zope.pluggableauth = 1.0.1
+zope.principalannotation = 3.6.0
+zope.principalregistry = 3.7.0
+zope.processlifetime = 1.0
+zope.proxy = 3.5.0
+zope.ptresource = 3.9.0
+zope.publisher = 3.12.0
+zope.ramcache = 1.0
+zope.schema = 3.6.1
+zope.security = 3.7.2
+zope.securitypolicy = 3.6.1
+zope.sendmail = 3.7.1
+zope.sequencesort = 3.4.0
+zope.server = 3.6.1
+zope.session = 3.9.2
+zope.site = 3.9.0
+zope.size = 3.4.1
+zope.structuredtext = 3.4.0
+zope.tal = 3.5.2
+zope.tales = 3.5.0
+zope.testbrowser = 3.7.0
+zope.testing = 3.8.7
+zope.traversing = 3.12.0
+zope.viewlet = 3.7.0
+zope.modulealias = 3.4.0
+zope.xmlpickle = 3.4.0
+ClientForm = 0.2.10
+distribute = 0.6.10
+docutils = 0.6
+Jinja2 = 2.2.1
+mechanize = 0.1.11
+py = 1.0.0
+Pygments = 1.2.2
+python-gettext = 1.0
+pytz = 2010b
+RestrictedPython = 3.5.1
+setuptools = 0.6c11
+Sphinx = 0.6.4
+transaction = 1.0.0
+z3c.recipe.sphinxdoc = 0.0.8
+zc.buildout = 1.4.3
+zc.lockfile = 1.0.0
+ZConfig = 2.7.1
+zc.recipe.egg = 1.2.2
+zc.recipe.testrunner = 1.2.0
+zc.resourcelibrary = 1.3.0
+zdaemon = 2.0.4
+ZODB3 = 3.9.4
+
+# grok 1.1
+grok = 1.1
+grokcore.annotation = 1.2
+grokcore.component = 1.8
+grokcore.content = 1.0
+grokcore.formlib = 1.5
+grokcore.message = 0.2
+grokcore.security = 1.4
+grokcore.site = 1.2
+grokcore.startup = 0.4
+grokcore.view = 1.13.2
+grokcore.viewlet = 1.4.1
+grokui.base = 0.2
+grokui.admin = 0.6.1
+martian = 0.11.1
+z3c.autoinclude = 0.3.2
+z3c.evalexception = 2.0
+z3c.flashmessage = 1.0
+zc.catalog = 1.4.2
+# Override
+zope.password = 3.6.0
+# Tools
+Paste = 1.7.2
+PasteDeploy = 1.3.3
+PasteScript = 1.7.3
+buildout.dumppickedversions = 0.4
+collective.recipe.template = 1.4
+simplejson = 2.0.9
+z3c.recipe.dev = 0.5.4
+z3c.recipe.eggbasket = 0.4.3
+z3c.recipe.i18n = 0.6.0
+z3c.recipe.mkdir = 0.3.1
+z3c.recipe.template = 0.1
+z3c.testsetup = 0.6.1
+zc.recipe.filestorage = 1.0.1
+zest.releaser = 3.4
Index: main/waeup.kofa/branches/uli-py3/layout/error-zope.html
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/error-zope.html	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/error-zope.html	(revision 16045)
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>waeup.kofa Portal - Error</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+    <link rel="shortcut icon" href="static/ico/favicon.ico">
+
+    <!-- stylesheets -->
+    <link href="static/bootstrap/css/bootstrap.min.css"
+	  rel="stylesheet">
+    <link href="static/css/base.css" rel="stylesheet">
+  </head>
+  <body id="kofa-body" class="errorpage">
+    <div class="container">
+      <div class="jumbotron">
+        <h1 id="kofa-error-heading">Error</h1>
+        <div id="kofa-error-message">Error Message</div>
+        <div>We are terribly sorry.</div>
+      </div>
+    </div>
+
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/layout/rules.xml
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/rules.xml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/rules.xml	(revision 16045)
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rules
+    xmlns="http://namespaces.plone.org/diazo"
+    xmlns:css="http://namespaces.plone.org/diazo/css"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <!-- Cases where we do not want themes... -->
+  <notheme if-path="/++grokui++" />  <!-- do not theme GrokUI -->
+  <notheme if-path="/_debug" /><!-- output by debugger -->
+  <notheme css:if-content="div#error-area" />
+
+  <!-- use error page if there is a <table class="layout">
+       tag in content. This tag is rendered in Zope 3 errors. -->
+  <rules css:if-content="table#layout">
+    <theme href="error-zope.html" />
+    <replace css:theme-children="#kofa-error-message"
+             css:content-children="h3" />
+  </rules>
+  <!-- use error page if there is no <div> tag in content.
+       This happens if something in the application went wrong. -->
+  <rules css:if-not-content="div">
+    <theme href="error-zope.html" />
+    <replace css:theme-children="#kofa-error-message"
+             css:content-children="body" />
+  </rules>
+
+  <!-- regular rules -->
+  <rules css:if-content="body.kofa-body">
+
+    <theme href="theme.html" />
+
+    <!-- head elements -->
+    <replace css:theme="html head title"
+             css:content="html head title" />
+
+    <!-- brand (upper left) -->
+    <copy attributes="href" css:theme="a.kofa-brand-link"
+          css:content="a.kofa-brand-link" />
+    <replace css:theme-children="a.kofa-brand-link"
+             css:content-children="a.kofa-brand-link" />
+
+    <!-- left top navigation -->
+    <replace css:theme-children="ul.kofa-navbar-left"
+             css:content-children="ul.kofa-navbar-left" />
+
+    <!-- right side top navigation -->
+    <replace css:theme-children="ul.kofa-navbar-right"
+             css:content-children="ul.kofa-navbar-right" />
+
+    <!-- maintenance mode warning -->
+    <drop css:theme="div.kofa-maintenance"
+          css:if-not-content="div.maintenance-warning" />
+    <replace css:theme-children="div.maintenance-warning"
+             css:content-children="div.maintenance-warning" />
+
+    <!-- breadcrumbs -->
+    <drop css:theme="div.kofa-breadcrumbs-box-long"
+          css:if-not-content="ul.breadcrumb" />
+    <drop css:theme="div.kofa-breadcrumbs-box-long"
+          css:if-content="div.wfstatus" />
+    <drop css:theme="div.kofa-breadcrumbs-box-short"
+          css:if-not-content="ul.breadcrumb" />
+    <drop css:theme="div.kofa-breadcrumbs-box-short"
+          css:if-not-content="div.wfstatus" />
+    <replace css:theme-children="ol.kofa-breadcrumbs"
+             css:content-children="ul.breadcrumb" />
+
+    <!-- student status -->
+    <drop css:theme="div.kofa-student-status-box"
+          css:if-not-content="div.wfstatus" />
+    <replace css:theme-children="div.wfstatus"
+             css:content-children="div.wfstatus" />
+
+    <!-- sidebar -->
+
+    <drop css:theme="div.kofa-sidebar"
+          css:if-not-content="div.sidebar" />
+    <replace css:theme-children="div.sidebar-nav"
+             css:content-children="div.sidebar" />
+
+    <!-- content -->
+
+    <!-- drop one of the both content areas, depending on sidebar -->
+
+    <drop css:theme="div.kofa-content-narrow"
+          css:if-not-content="div.sidebar" />
+    <drop css:theme="div.kofa-content-wide"
+          css:if-content="div.sidebar" />
+    <replace css:theme-children="div.kofa-content"
+             css:content-children="div.kofa-content" />
+
+
+    <!-- scripts -->
+    <drop css:theme="script.kofa-script-tooltip"
+          css:if-not-content="[data-toggle='tooltip']" />
+
+    <drop css:theme="script.kofa-script-datepicker"
+          css:if-not-content="input.datepicker-le-year" />
+
+    <drop css:theme="script.kofa-script-datatable"
+          css:if-not-content="table.kofa-data-table" />
+
+    <drop css:theme="script.kofa-filterByText"
+          css:if-not-content="input.kofa-filterbox" />
+
+<!--
+
+    <rules css:if-not-content="input.datepicker-le-year">
+        <drop css:theme="script.jquery-ui"
+              css:if-not-content="[data-toggle='tooltip']" />
+    </rules>
+
+-->
+<!--
+    <after css:theme-children="html head"
+           css:content="html head meta" />
+-->
+<!--
+    <after css:theme-children="html head"
+           css:content="html head base" />
+-->
+
+    <!-- top navigation -->
+    <!-- left top navigation -->
+<!--
+    <replace css:theme-children="ul.kofa-navbar-left"
+             css:content-children="ul.kofa-navbar-left" />
+-->
+
+    <!-- the language selector
+
+      quite tricky. We insert a <li> element at beginning of
+      ul.kofa-navbar-right. This <li> has a header element
+      (the 'Language' button) and the a number of <li> elements
+      representing the supported languages.
+      We change the input content of format
+
+         <a href="foo" title="Lang">Abbrev</a>
+
+      to
+
+         <a href="foo">Lang</a>
+
+      creating new nodes (<xsl:element>) with attributes copied over.
+    -->
+    <before css:theme-children="ul.kofa-navbar-right">
+      <li class="dropdown kofa-langselect">
+        <a href="#" class="dropdown-toggle"
+           data-toggle="dropdown">
+           <img src="/static/img/earth.png" />
+        </a>
+        <ul class="dropdown-menu">
+          <xsl:for-each css:select=" .language a">
+            <li class="kofa-language">
+              <xsl:element name="a">
+                <xsl:attribute name="href">
+                  <xsl:value-of select="@href" />
+                </xsl:attribute>
+                <xsl:value-of select="@title" />
+              </xsl:element>
+            </li>
+          </xsl:for-each>
+        </ul>
+      </li>
+    </before>
+
+    <!-- Make all data tables responsive -->
+
+    <replace css:content="table.kofa-data-table">
+      <div class="table-responsive">
+        <xsl:apply-templates css:select="table.kofa-data-table" mode="raw"/>
+      </div>
+    </replace>
+
+   </rules>
+
+</rules>
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.css	(revision 16045)
@@ -0,0 +1,397 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
+  background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #2d6ca2;
+  background-position: 0 -15px;
+}
+
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #2d6ca2;
+  border-color: #2b669a;
+}
+
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+}
+
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #357ebd;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+}
+
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
+  background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
+}
+
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
+}
+
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);
+  background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #222222 0%, #282828 100%);
+  background-image: linear-gradient(to bottom, #222222 0%, #282828 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
+}
+
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+}
+
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+}
+
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+}
+
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+}
+
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+}
+
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+}
+
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+}
+
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+}
+
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+}
+
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+}
+
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
+}
+
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #3071a9;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
+  background-repeat: repeat-x;
+  border-color: #3278b3;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
+}
+
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+}
+
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+}
+
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+}
+
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+}
+
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+}
+
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+}
+
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.min.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.min.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap-theme.min.css	(revision 16045)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)}
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.css	(revision 16045)
@@ -0,0 +1,7118 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+  display: block;
+}
+
+audio,
+canvas,
+video {
+  display: inline-block;
+}
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+[hidden],
+template {
+  display: none;
+}
+
+html {
+  font-family: sans-serif;
+  -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+}
+
+body {
+  margin: 0;
+}
+
+a {
+  background: transparent;
+}
+
+a:focus {
+  outline: thin dotted;
+}
+
+a:active,
+a:hover {
+  outline: 0;
+}
+
+h1 {
+  margin: 0.67em 0;
+  font-size: 2em;
+}
+
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+dfn {
+  font-style: italic;
+}
+
+hr {
+  height: 0;
+  -moz-box-sizing: content-box;
+       box-sizing: content-box;
+}
+
+mark {
+  color: #000;
+  background: #ff0;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, serif;
+  font-size: 1em;
+}
+
+pre {
+  white-space: pre-wrap;
+}
+
+q {
+  quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+small {
+  font-size: 80%;
+}
+
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -0.5em;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+img {
+  border: 0;
+}
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+figure {
+  margin: 0;
+}
+
+fieldset {
+  padding: 0.35em 0.625em 0.75em;
+  margin: 0 2px;
+  border: 1px solid #c0c0c0;
+}
+
+legend {
+  padding: 0;
+  border: 0;
+}
+
+button,
+input,
+select,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  font-size: 100%;
+}
+
+button,
+input {
+  line-height: normal;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer;
+  -webkit-appearance: button;
+}
+
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+  padding: 0;
+  box-sizing: border-box;
+}
+
+input[type="search"] {
+  -webkit-box-sizing: content-box;
+     -moz-box-sizing: content-box;
+          box-sizing: content-box;
+  -webkit-appearance: textfield;
+}
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}
+
+textarea {
+  overflow: auto;
+  vertical-align: top;
+}
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+@media print {
+  * {
+    color: #000 !important;
+    text-shadow: none !important;
+    background: transparent !important;
+    box-shadow: none !important;
+  }
+  a,
+  a:visited {
+    text-decoration: underline;
+  }
+  a[href]:after {
+    content: " (" attr(href) ")";
+  }
+  abbr[title]:after {
+    content: " (" attr(title) ")";
+  }
+  a[href^="javascript:"]:after,
+  a[href^="#"]:after {
+    content: "";
+  }
+  pre,
+  blockquote {
+    border: 1px solid #999;
+    page-break-inside: avoid;
+  }
+  thead {
+    display: table-header-group;
+  }
+  tr,
+  img {
+    page-break-inside: avoid;
+  }
+  img {
+    max-width: 100% !important;
+  }
+  @page  {
+    margin: 2cm .5cm;
+  }
+  p,
+  h2,
+  h3 {
+    orphans: 3;
+    widows: 3;
+  }
+  h2,
+  h3 {
+    page-break-after: avoid;
+  }
+  select {
+    background: #fff !important;
+  }
+  .navbar {
+    display: none;
+  }
+  .table td,
+  .table th {
+    background-color: #fff !important;
+  }
+  .btn > .caret,
+  .dropup > .btn > .caret {
+    border-top-color: #000 !important;
+  }
+  .label {
+    border: 1px solid #000;
+  }
+  .table {
+    border-collapse: collapse !important;
+  }
+  .table-bordered th,
+  .table-bordered td {
+    border: 1px solid #ddd !important;
+  }
+}
+
+*,
+*:before,
+*:after {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+html {
+  font-size: 62.5%;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+body {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #333333;
+  background-color: #ffffff;
+}
+
+input,
+button,
+select,
+textarea {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+a {
+  color: #428bca;
+  text-decoration: none;
+}
+
+a:hover,
+a:focus {
+  color: #2a6496;
+  text-decoration: underline;
+}
+
+a:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+img {
+  vertical-align: middle;
+}
+
+.img-responsive {
+  display: block;
+  height: auto;
+  max-width: 100%;
+}
+
+.img-rounded {
+  border-radius: 6px;
+}
+
+.img-thumbnail {
+  display: inline-block;
+  height: auto;
+  max-width: 100%;
+  padding: 4px;
+  line-height: 1.428571429;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-in-out;
+          transition: all 0.2s ease-in-out;
+}
+
+.img-circle {
+  border-radius: 50%;
+}
+
+hr {
+  margin-top: 20px;
+  margin-bottom: 20px;
+  border: 0;
+  border-top: 1px solid #eeeeee;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6 {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-weight: 500;
+  line-height: 1.1;
+  color: inherit;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small,
+.h1 small,
+.h2 small,
+.h3 small,
+.h4 small,
+.h5 small,
+.h6 small,
+h1 .small,
+h2 .small,
+h3 .small,
+h4 .small,
+h5 .small,
+h6 .small,
+.h1 .small,
+.h2 .small,
+.h3 .small,
+.h4 .small,
+.h5 .small,
+.h6 .small {
+  font-weight: normal;
+  line-height: 1;
+  color: #999999;
+}
+
+h1,
+h2,
+h3 {
+  margin-top: 20px;
+  margin-bottom: 10px;
+}
+
+h1 small,
+h2 small,
+h3 small,
+h1 .small,
+h2 .small,
+h3 .small {
+  font-size: 65%;
+}
+
+h4,
+h5,
+h6 {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+h4 small,
+h5 small,
+h6 small,
+h4 .small,
+h5 .small,
+h6 .small {
+  font-size: 75%;
+}
+
+h1,
+.h1 {
+  font-size: 36px;
+}
+
+h2,
+.h2 {
+  font-size: 30px;
+}
+
+h3,
+.h3 {
+  font-size: 24px;
+}
+
+h4,
+.h4 {
+  font-size: 18px;
+}
+
+h5,
+.h5 {
+  font-size: 14px;
+}
+
+h6,
+.h6 {
+  font-size: 12px;
+}
+
+p {
+  margin: 0 0 10px;
+}
+
+.lead {
+  margin-bottom: 20px;
+  font-size: 16px;
+  font-weight: 200;
+  line-height: 1.4;
+}
+
+@media (min-width: 768px) {
+  .lead {
+    font-size: 21px;
+  }
+}
+
+small,
+.small {
+  font-size: 85%;
+}
+
+cite {
+  font-style: normal;
+}
+
+.text-muted {
+  color: #999999;
+}
+
+.text-primary {
+  color: #428bca;
+}
+
+.text-primary:hover {
+  color: #3071a9;
+}
+
+.text-warning {
+  color: #8a6d3b;
+}
+
+.text-warning:hover {
+  color: #66512c;
+}
+
+.text-danger {
+  color: #a94442;
+}
+
+.text-danger:hover {
+  color: #843534;
+}
+
+.text-success {
+  color: #3c763d;
+}
+
+.text-success:hover {
+  color: #2b542c;
+}
+
+.text-info {
+  color: #31708f;
+}
+
+.text-info:hover {
+  color: #245269;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.page-header {
+  padding-bottom: 9px;
+  margin: 40px 0 20px;
+  border-bottom: 1px solid #eeeeee;
+}
+
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+ul ul,
+ol ul,
+ul ol,
+ol ol {
+  margin-bottom: 0;
+}
+
+.list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-inline {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-inline > li {
+  display: inline-block;
+  padding-right: 5px;
+  padding-left: 5px;
+}
+
+.list-inline > li:first-child {
+  padding-left: 0;
+}
+
+dl {
+  margin-top: 0;
+  margin-bottom: 20px;
+}
+
+dt,
+dd {
+  line-height: 1.428571429;
+}
+
+dt {
+  font-weight: bold;
+}
+
+dd {
+  margin-left: 0;
+}
+
+@media (min-width: 768px) {
+  .dl-horizontal dt {
+    float: left;
+    width: 160px;
+    overflow: hidden;
+    clear: left;
+    text-align: right;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .dl-horizontal dd {
+    margin-left: 180px;
+  }
+  .dl-horizontal dd:before,
+  .dl-horizontal dd:after {
+    display: table;
+    content: " ";
+  }
+  .dl-horizontal dd:after {
+    clear: both;
+  }
+  .dl-horizontal dd:before,
+  .dl-horizontal dd:after {
+    display: table;
+    content: " ";
+  }
+  .dl-horizontal dd:after {
+    clear: both;
+  }
+}
+
+abbr[title],
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted #999999;
+}
+
+.initialism {
+  font-size: 90%;
+  text-transform: uppercase;
+}
+
+blockquote {
+  padding: 10px 20px;
+  margin: 0 0 20px;
+  border-left: 5px solid #eeeeee;
+}
+
+blockquote p {
+  font-size: 17.5px;
+  font-weight: 300;
+  line-height: 1.25;
+}
+
+blockquote p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote small,
+blockquote .small {
+  display: block;
+  line-height: 1.428571429;
+  color: #999999;
+}
+
+blockquote small:before,
+blockquote .small:before {
+  content: '\2014 \00A0';
+}
+
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  border-right: 5px solid #eeeeee;
+  border-left: 0;
+}
+
+blockquote.pull-right p,
+blockquote.pull-right small,
+blockquote.pull-right .small {
+  text-align: right;
+}
+
+blockquote.pull-right small:before,
+blockquote.pull-right .small:before {
+  content: '';
+}
+
+blockquote.pull-right small:after,
+blockquote.pull-right .small:after {
+  content: '\00A0 \2014';
+}
+
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+
+address {
+  margin-bottom: 20px;
+  font-style: normal;
+  line-height: 1.428571429;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+}
+
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: #c7254e;
+  white-space: nowrap;
+  background-color: #f9f2f4;
+  border-radius: 4px;
+}
+
+pre {
+  display: block;
+  padding: 9.5px;
+  margin: 0 0 10px;
+  font-size: 13px;
+  line-height: 1.428571429;
+  color: #333333;
+  word-break: break-all;
+  word-wrap: break-word;
+  background-color: #f5f5f5;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+}
+
+pre code {
+  padding: 0;
+  font-size: inherit;
+  color: inherit;
+  white-space: pre-wrap;
+  background-color: transparent;
+  border-radius: 0;
+}
+
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+
+.container {
+  padding-right: 15px;
+  padding-left: 15px;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+.container:before,
+.container:after {
+  display: table;
+  content: " ";
+}
+
+.container:after {
+  clear: both;
+}
+
+.container:before,
+.container:after {
+  display: table;
+  content: " ";
+}
+
+.container:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .container {
+    width: 750px;
+  }
+}
+
+@media (min-width: 992px) {
+  .container {
+    width: 970px;
+  }
+}
+
+@media (min-width: 1200px) {
+  .container {
+    width: 1170px;
+  }
+}
+
+.row {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+.row:before,
+.row:after {
+  display: table;
+  content: " ";
+}
+
+.row:after {
+  clear: both;
+}
+
+.row:before,
+.row:after {
+  display: table;
+  content: " ";
+}
+
+.row:after {
+  clear: both;
+}
+
+.col-xs-1,
+.col-sm-1,
+.col-md-1,
+.col-lg-1,
+.col-xs-2,
+.col-sm-2,
+.col-md-2,
+.col-lg-2,
+.col-xs-3,
+.col-sm-3,
+.col-md-3,
+.col-lg-3,
+.col-xs-4,
+.col-sm-4,
+.col-md-4,
+.col-lg-4,
+.col-xs-5,
+.col-sm-5,
+.col-md-5,
+.col-lg-5,
+.col-xs-6,
+.col-sm-6,
+.col-md-6,
+.col-lg-6,
+.col-xs-7,
+.col-sm-7,
+.col-md-7,
+.col-lg-7,
+.col-xs-8,
+.col-sm-8,
+.col-md-8,
+.col-lg-8,
+.col-xs-9,
+.col-sm-9,
+.col-md-9,
+.col-lg-9,
+.col-xs-10,
+.col-sm-10,
+.col-md-10,
+.col-lg-10,
+.col-xs-11,
+.col-sm-11,
+.col-md-11,
+.col-lg-11,
+.col-xs-12,
+.col-sm-12,
+.col-md-12,
+.col-lg-12 {
+  position: relative;
+  min-height: 1px;
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+.col-xs-1,
+.col-xs-2,
+.col-xs-3,
+.col-xs-4,
+.col-xs-5,
+.col-xs-6,
+.col-xs-7,
+.col-xs-8,
+.col-xs-9,
+.col-xs-10,
+.col-xs-11,
+.col-xs-12 {
+  float: left;
+}
+
+.col-xs-12 {
+  width: 100%;
+}
+
+.col-xs-11 {
+  width: 91.66666666666666%;
+}
+
+.col-xs-10 {
+  width: 83.33333333333334%;
+}
+
+.col-xs-9 {
+  width: 75%;
+}
+
+.col-xs-8 {
+  width: 66.66666666666666%;
+}
+
+.col-xs-7 {
+  width: 58.333333333333336%;
+}
+
+.col-xs-6 {
+  width: 50%;
+}
+
+.col-xs-5 {
+  width: 41.66666666666667%;
+}
+
+.col-xs-4 {
+  width: 33.33333333333333%;
+}
+
+.col-xs-3 {
+  width: 25%;
+}
+
+.col-xs-2 {
+  width: 16.666666666666664%;
+}
+
+.col-xs-1 {
+  width: 8.333333333333332%;
+}
+
+.col-xs-pull-12 {
+  right: 100%;
+}
+
+.col-xs-pull-11 {
+  right: 91.66666666666666%;
+}
+
+.col-xs-pull-10 {
+  right: 83.33333333333334%;
+}
+
+.col-xs-pull-9 {
+  right: 75%;
+}
+
+.col-xs-pull-8 {
+  right: 66.66666666666666%;
+}
+
+.col-xs-pull-7 {
+  right: 58.333333333333336%;
+}
+
+.col-xs-pull-6 {
+  right: 50%;
+}
+
+.col-xs-pull-5 {
+  right: 41.66666666666667%;
+}
+
+.col-xs-pull-4 {
+  right: 33.33333333333333%;
+}
+
+.col-xs-pull-3 {
+  right: 25%;
+}
+
+.col-xs-pull-2 {
+  right: 16.666666666666664%;
+}
+
+.col-xs-pull-1 {
+  right: 8.333333333333332%;
+}
+
+.col-xs-pull-0 {
+  right: 0;
+}
+
+.col-xs-push-12 {
+  left: 100%;
+}
+
+.col-xs-push-11 {
+  left: 91.66666666666666%;
+}
+
+.col-xs-push-10 {
+  left: 83.33333333333334%;
+}
+
+.col-xs-push-9 {
+  left: 75%;
+}
+
+.col-xs-push-8 {
+  left: 66.66666666666666%;
+}
+
+.col-xs-push-7 {
+  left: 58.333333333333336%;
+}
+
+.col-xs-push-6 {
+  left: 50%;
+}
+
+.col-xs-push-5 {
+  left: 41.66666666666667%;
+}
+
+.col-xs-push-4 {
+  left: 33.33333333333333%;
+}
+
+.col-xs-push-3 {
+  left: 25%;
+}
+
+.col-xs-push-2 {
+  left: 16.666666666666664%;
+}
+
+.col-xs-push-1 {
+  left: 8.333333333333332%;
+}
+
+.col-xs-push-0 {
+  left: 0;
+}
+
+.col-xs-offset-12 {
+  margin-left: 100%;
+}
+
+.col-xs-offset-11 {
+  margin-left: 91.66666666666666%;
+}
+
+.col-xs-offset-10 {
+  margin-left: 83.33333333333334%;
+}
+
+.col-xs-offset-9 {
+  margin-left: 75%;
+}
+
+.col-xs-offset-8 {
+  margin-left: 66.66666666666666%;
+}
+
+.col-xs-offset-7 {
+  margin-left: 58.333333333333336%;
+}
+
+.col-xs-offset-6 {
+  margin-left: 50%;
+}
+
+.col-xs-offset-5 {
+  margin-left: 41.66666666666667%;
+}
+
+.col-xs-offset-4 {
+  margin-left: 33.33333333333333%;
+}
+
+.col-xs-offset-3 {
+  margin-left: 25%;
+}
+
+.col-xs-offset-2 {
+  margin-left: 16.666666666666664%;
+}
+
+.col-xs-offset-1 {
+  margin-left: 8.333333333333332%;
+}
+
+.col-xs-offset-0 {
+  margin-left: 0;
+}
+
+@media (min-width: 768px) {
+  .col-sm-1,
+  .col-sm-2,
+  .col-sm-3,
+  .col-sm-4,
+  .col-sm-5,
+  .col-sm-6,
+  .col-sm-7,
+  .col-sm-8,
+  .col-sm-9,
+  .col-sm-10,
+  .col-sm-11,
+  .col-sm-12 {
+    float: left;
+  }
+  .col-sm-12 {
+    width: 100%;
+  }
+  .col-sm-11 {
+    width: 91.66666666666666%;
+  }
+  .col-sm-10 {
+    width: 83.33333333333334%;
+  }
+  .col-sm-9 {
+    width: 75%;
+  }
+  .col-sm-8 {
+    width: 66.66666666666666%;
+  }
+  .col-sm-7 {
+    width: 58.333333333333336%;
+  }
+  .col-sm-6 {
+    width: 50%;
+  }
+  .col-sm-5 {
+    width: 41.66666666666667%;
+  }
+  .col-sm-4 {
+    width: 33.33333333333333%;
+  }
+  .col-sm-3 {
+    width: 25%;
+  }
+  .col-sm-2 {
+    width: 16.666666666666664%;
+  }
+  .col-sm-1 {
+    width: 8.333333333333332%;
+  }
+  .col-sm-pull-12 {
+    right: 100%;
+  }
+  .col-sm-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-sm-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-sm-pull-9 {
+    right: 75%;
+  }
+  .col-sm-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-sm-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-sm-pull-6 {
+    right: 50%;
+  }
+  .col-sm-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-sm-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-sm-pull-3 {
+    right: 25%;
+  }
+  .col-sm-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-sm-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-sm-pull-0 {
+    right: 0;
+  }
+  .col-sm-push-12 {
+    left: 100%;
+  }
+  .col-sm-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-sm-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-sm-push-9 {
+    left: 75%;
+  }
+  .col-sm-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-sm-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-sm-push-6 {
+    left: 50%;
+  }
+  .col-sm-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-sm-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-sm-push-3 {
+    left: 25%;
+  }
+  .col-sm-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-sm-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-sm-push-0 {
+    left: 0;
+  }
+  .col-sm-offset-12 {
+    margin-left: 100%;
+  }
+  .col-sm-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-sm-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-sm-offset-9 {
+    margin-left: 75%;
+  }
+  .col-sm-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-sm-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-sm-offset-6 {
+    margin-left: 50%;
+  }
+  .col-sm-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-sm-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-sm-offset-3 {
+    margin-left: 25%;
+  }
+  .col-sm-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-sm-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-sm-offset-0 {
+    margin-left: 0;
+  }
+}
+
+@media (min-width: 992px) {
+  .col-md-1,
+  .col-md-2,
+  .col-md-3,
+  .col-md-4,
+  .col-md-5,
+  .col-md-6,
+  .col-md-7,
+  .col-md-8,
+  .col-md-9,
+  .col-md-10,
+  .col-md-11,
+  .col-md-12 {
+    float: left;
+  }
+  .col-md-12 {
+    width: 100%;
+  }
+  .col-md-11 {
+    width: 91.66666666666666%;
+  }
+  .col-md-10 {
+    width: 83.33333333333334%;
+  }
+  .col-md-9 {
+    width: 75%;
+  }
+  .col-md-8 {
+    width: 66.66666666666666%;
+  }
+  .col-md-7 {
+    width: 58.333333333333336%;
+  }
+  .col-md-6 {
+    width: 50%;
+  }
+  .col-md-5 {
+    width: 41.66666666666667%;
+  }
+  .col-md-4 {
+    width: 33.33333333333333%;
+  }
+  .col-md-3 {
+    width: 25%;
+  }
+  .col-md-2 {
+    width: 16.666666666666664%;
+  }
+  .col-md-1 {
+    width: 8.333333333333332%;
+  }
+  .col-md-pull-12 {
+    right: 100%;
+  }
+  .col-md-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-md-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-md-pull-9 {
+    right: 75%;
+  }
+  .col-md-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-md-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-md-pull-6 {
+    right: 50%;
+  }
+  .col-md-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-md-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-md-pull-3 {
+    right: 25%;
+  }
+  .col-md-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-md-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-md-pull-0 {
+    right: 0;
+  }
+  .col-md-push-12 {
+    left: 100%;
+  }
+  .col-md-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-md-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-md-push-9 {
+    left: 75%;
+  }
+  .col-md-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-md-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-md-push-6 {
+    left: 50%;
+  }
+  .col-md-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-md-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-md-push-3 {
+    left: 25%;
+  }
+  .col-md-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-md-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-md-push-0 {
+    left: 0;
+  }
+  .col-md-offset-12 {
+    margin-left: 100%;
+  }
+  .col-md-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-md-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-md-offset-9 {
+    margin-left: 75%;
+  }
+  .col-md-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-md-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-md-offset-6 {
+    margin-left: 50%;
+  }
+  .col-md-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-md-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-md-offset-3 {
+    margin-left: 25%;
+  }
+  .col-md-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-md-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-md-offset-0 {
+    margin-left: 0;
+  }
+}
+
+@media (min-width: 1200px) {
+  .col-lg-1,
+  .col-lg-2,
+  .col-lg-3,
+  .col-lg-4,
+  .col-lg-5,
+  .col-lg-6,
+  .col-lg-7,
+  .col-lg-8,
+  .col-lg-9,
+  .col-lg-10,
+  .col-lg-11,
+  .col-lg-12 {
+    float: left;
+  }
+  .col-lg-12 {
+    width: 100%;
+  }
+  .col-lg-11 {
+    width: 91.66666666666666%;
+  }
+  .col-lg-10 {
+    width: 83.33333333333334%;
+  }
+  .col-lg-9 {
+    width: 75%;
+  }
+  .col-lg-8 {
+    width: 66.66666666666666%;
+  }
+  .col-lg-7 {
+    width: 58.333333333333336%;
+  }
+  .col-lg-6 {
+    width: 50%;
+  }
+  .col-lg-5 {
+    width: 41.66666666666667%;
+  }
+  .col-lg-4 {
+    width: 33.33333333333333%;
+  }
+  .col-lg-3 {
+    width: 25%;
+  }
+  .col-lg-2 {
+    width: 16.666666666666664%;
+  }
+  .col-lg-1 {
+    width: 8.333333333333332%;
+  }
+  .col-lg-pull-12 {
+    right: 100%;
+  }
+  .col-lg-pull-11 {
+    right: 91.66666666666666%;
+  }
+  .col-lg-pull-10 {
+    right: 83.33333333333334%;
+  }
+  .col-lg-pull-9 {
+    right: 75%;
+  }
+  .col-lg-pull-8 {
+    right: 66.66666666666666%;
+  }
+  .col-lg-pull-7 {
+    right: 58.333333333333336%;
+  }
+  .col-lg-pull-6 {
+    right: 50%;
+  }
+  .col-lg-pull-5 {
+    right: 41.66666666666667%;
+  }
+  .col-lg-pull-4 {
+    right: 33.33333333333333%;
+  }
+  .col-lg-pull-3 {
+    right: 25%;
+  }
+  .col-lg-pull-2 {
+    right: 16.666666666666664%;
+  }
+  .col-lg-pull-1 {
+    right: 8.333333333333332%;
+  }
+  .col-lg-pull-0 {
+    right: 0;
+  }
+  .col-lg-push-12 {
+    left: 100%;
+  }
+  .col-lg-push-11 {
+    left: 91.66666666666666%;
+  }
+  .col-lg-push-10 {
+    left: 83.33333333333334%;
+  }
+  .col-lg-push-9 {
+    left: 75%;
+  }
+  .col-lg-push-8 {
+    left: 66.66666666666666%;
+  }
+  .col-lg-push-7 {
+    left: 58.333333333333336%;
+  }
+  .col-lg-push-6 {
+    left: 50%;
+  }
+  .col-lg-push-5 {
+    left: 41.66666666666667%;
+  }
+  .col-lg-push-4 {
+    left: 33.33333333333333%;
+  }
+  .col-lg-push-3 {
+    left: 25%;
+  }
+  .col-lg-push-2 {
+    left: 16.666666666666664%;
+  }
+  .col-lg-push-1 {
+    left: 8.333333333333332%;
+  }
+  .col-lg-push-0 {
+    left: 0;
+  }
+  .col-lg-offset-12 {
+    margin-left: 100%;
+  }
+  .col-lg-offset-11 {
+    margin-left: 91.66666666666666%;
+  }
+  .col-lg-offset-10 {
+    margin-left: 83.33333333333334%;
+  }
+  .col-lg-offset-9 {
+    margin-left: 75%;
+  }
+  .col-lg-offset-8 {
+    margin-left: 66.66666666666666%;
+  }
+  .col-lg-offset-7 {
+    margin-left: 58.333333333333336%;
+  }
+  .col-lg-offset-6 {
+    margin-left: 50%;
+  }
+  .col-lg-offset-5 {
+    margin-left: 41.66666666666667%;
+  }
+  .col-lg-offset-4 {
+    margin-left: 33.33333333333333%;
+  }
+  .col-lg-offset-3 {
+    margin-left: 25%;
+  }
+  .col-lg-offset-2 {
+    margin-left: 16.666666666666664%;
+  }
+  .col-lg-offset-1 {
+    margin-left: 8.333333333333332%;
+  }
+  .col-lg-offset-0 {
+    margin-left: 0;
+  }
+}
+
+table {
+  max-width: 100%;
+  background-color: transparent;
+}
+
+th {
+  text-align: left;
+}
+
+.table {
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  padding: 8px;
+  line-height: 1.428571429;
+  vertical-align: top;
+  border-top: 1px solid #dddddd;
+}
+
+.table > thead > tr > th {
+  vertical-align: bottom;
+  border-bottom: 2px solid #dddddd;
+}
+
+.table > caption + thead > tr:first-child > th,
+.table > colgroup + thead > tr:first-child > th,
+.table > thead:first-child > tr:first-child > th,
+.table > caption + thead > tr:first-child > td,
+.table > colgroup + thead > tr:first-child > td,
+.table > thead:first-child > tr:first-child > td {
+  border-top: 0;
+}
+
+.table > tbody + tbody {
+  border-top: 2px solid #dddddd;
+}
+
+.table .table {
+  background-color: #ffffff;
+}
+
+.table-condensed > thead > tr > th,
+.table-condensed > tbody > tr > th,
+.table-condensed > tfoot > tr > th,
+.table-condensed > thead > tr > td,
+.table-condensed > tbody > tr > td,
+.table-condensed > tfoot > tr > td {
+  padding: 5px;
+}
+
+.table-bordered {
+  border: 1px solid #dddddd;
+}
+
+.table-bordered > thead > tr > th,
+.table-bordered > tbody > tr > th,
+.table-bordered > tfoot > tr > th,
+.table-bordered > thead > tr > td,
+.table-bordered > tbody > tr > td,
+.table-bordered > tfoot > tr > td {
+  border: 1px solid #dddddd;
+}
+
+.table-bordered > thead > tr > th,
+.table-bordered > thead > tr > td {
+  border-bottom-width: 2px;
+}
+
+.table-striped > tbody > tr:nth-child(odd) > td,
+.table-striped > tbody > tr:nth-child(odd) > th {
+  background-color: #f9f9f9;
+}
+
+.table-hover > tbody > tr:hover > td,
+.table-hover > tbody > tr:hover > th {
+  background-color: #f5f5f5;
+}
+
+table col[class*="col-"] {
+  position: static;
+  display: table-column;
+  float: none;
+}
+
+table td[class*="col-"],
+table th[class*="col-"] {
+  display: table-cell;
+  float: none;
+}
+
+.table > thead > tr > .active,
+.table > tbody > tr > .active,
+.table > tfoot > tr > .active,
+.table > thead > .active > td,
+.table > tbody > .active > td,
+.table > tfoot > .active > td,
+.table > thead > .active > th,
+.table > tbody > .active > th,
+.table > tfoot > .active > th {
+  background-color: #f5f5f5;
+}
+
+.table-hover > tbody > tr > .active:hover,
+.table-hover > tbody > .active:hover > td,
+.table-hover > tbody > .active:hover > th {
+  background-color: #e8e8e8;
+}
+
+.table > thead > tr > .success,
+.table > tbody > tr > .success,
+.table > tfoot > tr > .success,
+.table > thead > .success > td,
+.table > tbody > .success > td,
+.table > tfoot > .success > td,
+.table > thead > .success > th,
+.table > tbody > .success > th,
+.table > tfoot > .success > th {
+  background-color: #dff0d8;
+}
+
+.table-hover > tbody > tr > .success:hover,
+.table-hover > tbody > .success:hover > td,
+.table-hover > tbody > .success:hover > th {
+  background-color: #d0e9c6;
+}
+
+.table > thead > tr > .danger,
+.table > tbody > tr > .danger,
+.table > tfoot > tr > .danger,
+.table > thead > .danger > td,
+.table > tbody > .danger > td,
+.table > tfoot > .danger > td,
+.table > thead > .danger > th,
+.table > tbody > .danger > th,
+.table > tfoot > .danger > th {
+  background-color: #f2dede;
+}
+
+.table-hover > tbody > tr > .danger:hover,
+.table-hover > tbody > .danger:hover > td,
+.table-hover > tbody > .danger:hover > th {
+  background-color: #ebcccc;
+}
+
+.table > thead > tr > .warning,
+.table > tbody > tr > .warning,
+.table > tfoot > tr > .warning,
+.table > thead > .warning > td,
+.table > tbody > .warning > td,
+.table > tfoot > .warning > td,
+.table > thead > .warning > th,
+.table > tbody > .warning > th,
+.table > tfoot > .warning > th {
+  background-color: #fcf8e3;
+}
+
+.table-hover > tbody > tr > .warning:hover,
+.table-hover > tbody > .warning:hover > td,
+.table-hover > tbody > .warning:hover > th {
+  background-color: #faf2cc;
+}
+
+@media (max-width: 767px) {
+  .table-responsive {
+    width: 100%;
+    margin-bottom: 15px;
+    overflow-x: scroll;
+    overflow-y: hidden;
+    border: 1px solid #dddddd;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    -webkit-overflow-scrolling: touch;
+  }
+  .table-responsive > .table {
+    margin-bottom: 0;
+  }
+  .table-responsive > .table > thead > tr > th,
+  .table-responsive > .table > tbody > tr > th,
+  .table-responsive > .table > tfoot > tr > th,
+  .table-responsive > .table > thead > tr > td,
+  .table-responsive > .table > tbody > tr > td,
+  .table-responsive > .table > tfoot > tr > td {
+    white-space: nowrap;
+  }
+  .table-responsive > .table-bordered {
+    border: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:first-child,
+  .table-responsive > .table-bordered > tbody > tr > th:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+  .table-responsive > .table-bordered > thead > tr > td:first-child,
+  .table-responsive > .table-bordered > tbody > tr > td:first-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+    border-left: 0;
+  }
+  .table-responsive > .table-bordered > thead > tr > th:last-child,
+  .table-responsive > .table-bordered > tbody > tr > th:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+  .table-responsive > .table-bordered > thead > tr > td:last-child,
+  .table-responsive > .table-bordered > tbody > tr > td:last-child,
+  .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+    border-right: 0;
+  }
+  .table-responsive > .table-bordered > tbody > tr:last-child > th,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+  .table-responsive > .table-bordered > tbody > tr:last-child > td,
+  .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+    border-bottom: 0;
+  }
+}
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 20px;
+  font-size: 21px;
+  line-height: inherit;
+  color: #333333;
+  border: 0;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+label {
+  display: inline-block;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+input[type="search"] {
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9;
+  /* IE8-9 */
+
+  line-height: normal;
+}
+
+input[type="file"] {
+  display: block;
+}
+
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+select optgroup {
+  font-family: inherit;
+  font-size: inherit;
+  font-style: inherit;
+}
+
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+  height: auto;
+}
+
+output {
+  display: block;
+  padding-top: 7px;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #555555;
+  vertical-align: middle;
+}
+
+.form-control {
+  display: block;
+  width: 100%;
+  height: 34px;
+  padding: 6px 12px;
+  font-size: 14px;
+  line-height: 1.428571429;
+  color: #555555;
+  vertical-align: middle;
+  background-color: #ffffff;
+  background-image: none;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+          transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+
+.form-control:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.form-control:-moz-placeholder {
+  color: #999999;
+}
+
+.form-control::-moz-placeholder {
+  color: #999999;
+  opacity: 1;
+}
+
+.form-control:-ms-input-placeholder {
+  color: #999999;
+}
+
+.form-control::-webkit-input-placeholder {
+  color: #999999;
+}
+
+.form-control[disabled],
+.form-control[readonly],
+fieldset[disabled] .form-control {
+  cursor: not-allowed;
+  background-color: #eeeeee;
+}
+
+textarea.form-control {
+  height: auto;
+}
+
+.form-group {
+  margin-bottom: 15px;
+}
+
+.radio,
+.checkbox {
+  display: block;
+  min-height: 20px;
+  padding-left: 20px;
+  margin-top: 10px;
+  margin-bottom: 10px;
+  vertical-align: middle;
+}
+
+.radio label,
+.checkbox label {
+  display: inline;
+  margin-bottom: 0;
+  font-weight: normal;
+  cursor: pointer;
+}
+
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  float: left;
+  margin-left: -20px;
+}
+
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px;
+}
+
+.radio-inline,
+.checkbox-inline {
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  font-weight: normal;
+  vertical-align: middle;
+  cursor: pointer;
+}
+
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px;
+}
+
+input[type="radio"][disabled],
+input[type="checkbox"][disabled],
+.radio[disabled],
+.radio-inline[disabled],
+.checkbox[disabled],
+.checkbox-inline[disabled],
+fieldset[disabled] input[type="radio"],
+fieldset[disabled] input[type="checkbox"],
+fieldset[disabled] .radio,
+fieldset[disabled] .radio-inline,
+fieldset[disabled] .checkbox,
+fieldset[disabled] .checkbox-inline {
+  cursor: not-allowed;
+}
+
+.input-sm {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+select.input-sm {
+  height: 30px;
+  line-height: 30px;
+}
+
+textarea.input-sm {
+  height: auto;
+}
+
+.input-lg {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+select.input-lg {
+  height: 46px;
+  line-height: 46px;
+}
+
+textarea.input-lg {
+  height: auto;
+}
+
+.has-warning .help-block,
+.has-warning .control-label,
+.has-warning .radio,
+.has-warning .checkbox,
+.has-warning .radio-inline,
+.has-warning .checkbox-inline {
+  color: #8a6d3b;
+}
+
+.has-warning .form-control {
+  border-color: #8a6d3b;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-warning .form-control:focus {
+  border-color: #66512c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+}
+
+.has-warning .input-group-addon {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #8a6d3b;
+}
+
+.has-error .help-block,
+.has-error .control-label,
+.has-error .radio,
+.has-error .checkbox,
+.has-error .radio-inline,
+.has-error .checkbox-inline {
+  color: #a94442;
+}
+
+.has-error .form-control {
+  border-color: #a94442;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-error .form-control:focus {
+  border-color: #843534;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+}
+
+.has-error .input-group-addon {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #a94442;
+}
+
+.has-success .help-block,
+.has-success .control-label,
+.has-success .radio,
+.has-success .checkbox,
+.has-success .radio-inline,
+.has-success .checkbox-inline {
+  color: #3c763d;
+}
+
+.has-success .form-control {
+  border-color: #3c763d;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+
+.has-success .form-control:focus {
+  border-color: #2b542c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+}
+
+.has-success .input-group-addon {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #3c763d;
+}
+
+.form-control-static {
+  margin-bottom: 0;
+}
+
+.help-block {
+  display: block;
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: #737373;
+}
+
+@media (min-width: 768px) {
+  .form-inline .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .form-inline .form-control {
+    display: inline-block;
+  }
+  .form-inline select.form-control {
+    width: auto;
+  }
+  .form-inline .radio,
+  .form-inline .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+  .form-inline .radio input[type="radio"],
+  .form-inline .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+}
+
+.form-horizontal .control-label,
+.form-horizontal .radio,
+.form-horizontal .checkbox,
+.form-horizontal .radio-inline,
+.form-horizontal .checkbox-inline {
+  padding-top: 7px;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.form-horizontal .radio,
+.form-horizontal .checkbox {
+  min-height: 27px;
+}
+
+.form-horizontal .form-group {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+.form-horizontal .form-group:before,
+.form-horizontal .form-group:after {
+  display: table;
+  content: " ";
+}
+
+.form-horizontal .form-group:after {
+  clear: both;
+}
+
+.form-horizontal .form-group:before,
+.form-horizontal .form-group:after {
+  display: table;
+  content: " ";
+}
+
+.form-horizontal .form-group:after {
+  clear: both;
+}
+
+.form-horizontal .form-control-static {
+  padding-top: 7px;
+}
+
+@media (min-width: 768px) {
+  .form-horizontal .control-label {
+    text-align: right;
+  }
+}
+
+.btn {
+  display: inline-block;
+  padding: 6px 12px;
+  margin-bottom: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1.428571429;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+       -o-user-select: none;
+          user-select: none;
+}
+
+.btn:focus {
+  outline: thin dotted;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+.btn:hover,
+.btn:focus {
+  color: #333333;
+  text-decoration: none;
+}
+
+.btn:active,
+.btn.active {
+  background-image: none;
+  outline: 0;
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn.disabled,
+.btn[disabled],
+fieldset[disabled] .btn {
+  pointer-events: none;
+  cursor: not-allowed;
+  opacity: 0.65;
+  filter: alpha(opacity=65);
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn-default {
+  color: #333333;
+  background-color: #ffffff;
+  border-color: #cccccc;
+}
+
+.btn-default:hover,
+.btn-default:focus,
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  color: #333333;
+  background-color: #ebebeb;
+  border-color: #adadad;
+}
+
+.btn-default:active,
+.btn-default.active,
+.open .dropdown-toggle.btn-default {
+  background-image: none;
+}
+
+.btn-default.disabled,
+.btn-default[disabled],
+fieldset[disabled] .btn-default,
+.btn-default.disabled:hover,
+.btn-default[disabled]:hover,
+fieldset[disabled] .btn-default:hover,
+.btn-default.disabled:focus,
+.btn-default[disabled]:focus,
+fieldset[disabled] .btn-default:focus,
+.btn-default.disabled:active,
+.btn-default[disabled]:active,
+fieldset[disabled] .btn-default:active,
+.btn-default.disabled.active,
+.btn-default[disabled].active,
+fieldset[disabled] .btn-default.active {
+  background-color: #ffffff;
+  border-color: #cccccc;
+}
+
+.btn-default .badge {
+  color: #ffffff;
+  background-color: #fff;
+}
+
+.btn-primary {
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  color: #ffffff;
+  background-color: #3276b1;
+  border-color: #285e8e;
+}
+
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+  background-image: none;
+}
+
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: #428bca;
+  border-color: #357ebd;
+}
+
+.btn-primary .badge {
+  color: #428bca;
+  background-color: #fff;
+}
+
+.btn-warning {
+  color: #ffffff;
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+
+.btn-warning:hover,
+.btn-warning:focus,
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  color: #ffffff;
+  background-color: #ed9c28;
+  border-color: #d58512;
+}
+
+.btn-warning:active,
+.btn-warning.active,
+.open .dropdown-toggle.btn-warning {
+  background-image: none;
+}
+
+.btn-warning.disabled,
+.btn-warning[disabled],
+fieldset[disabled] .btn-warning,
+.btn-warning.disabled:hover,
+.btn-warning[disabled]:hover,
+fieldset[disabled] .btn-warning:hover,
+.btn-warning.disabled:focus,
+.btn-warning[disabled]:focus,
+fieldset[disabled] .btn-warning:focus,
+.btn-warning.disabled:active,
+.btn-warning[disabled]:active,
+fieldset[disabled] .btn-warning:active,
+.btn-warning.disabled.active,
+.btn-warning[disabled].active,
+fieldset[disabled] .btn-warning.active {
+  background-color: #f0ad4e;
+  border-color: #eea236;
+}
+
+.btn-warning .badge {
+  color: #f0ad4e;
+  background-color: #fff;
+}
+
+.btn-danger {
+  color: #ffffff;
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+
+.btn-danger:hover,
+.btn-danger:focus,
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  color: #ffffff;
+  background-color: #d2322d;
+  border-color: #ac2925;
+}
+
+.btn-danger:active,
+.btn-danger.active,
+.open .dropdown-toggle.btn-danger {
+  background-image: none;
+}
+
+.btn-danger.disabled,
+.btn-danger[disabled],
+fieldset[disabled] .btn-danger,
+.btn-danger.disabled:hover,
+.btn-danger[disabled]:hover,
+fieldset[disabled] .btn-danger:hover,
+.btn-danger.disabled:focus,
+.btn-danger[disabled]:focus,
+fieldset[disabled] .btn-danger:focus,
+.btn-danger.disabled:active,
+.btn-danger[disabled]:active,
+fieldset[disabled] .btn-danger:active,
+.btn-danger.disabled.active,
+.btn-danger[disabled].active,
+fieldset[disabled] .btn-danger.active {
+  background-color: #d9534f;
+  border-color: #d43f3a;
+}
+
+.btn-danger .badge {
+  color: #d9534f;
+  background-color: #fff;
+}
+
+.btn-success {
+  color: #ffffff;
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+
+.btn-success:hover,
+.btn-success:focus,
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  color: #ffffff;
+  background-color: #47a447;
+  border-color: #398439;
+}
+
+.btn-success:active,
+.btn-success.active,
+.open .dropdown-toggle.btn-success {
+  background-image: none;
+}
+
+.btn-success.disabled,
+.btn-success[disabled],
+fieldset[disabled] .btn-success,
+.btn-success.disabled:hover,
+.btn-success[disabled]:hover,
+fieldset[disabled] .btn-success:hover,
+.btn-success.disabled:focus,
+.btn-success[disabled]:focus,
+fieldset[disabled] .btn-success:focus,
+.btn-success.disabled:active,
+.btn-success[disabled]:active,
+fieldset[disabled] .btn-success:active,
+.btn-success.disabled.active,
+.btn-success[disabled].active,
+fieldset[disabled] .btn-success.active {
+  background-color: #5cb85c;
+  border-color: #4cae4c;
+}
+
+.btn-success .badge {
+  color: #5cb85c;
+  background-color: #fff;
+}
+
+.btn-info {
+  color: #ffffff;
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+
+.btn-info:hover,
+.btn-info:focus,
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  color: #ffffff;
+  background-color: #39b3d7;
+  border-color: #269abc;
+}
+
+.btn-info:active,
+.btn-info.active,
+.open .dropdown-toggle.btn-info {
+  background-image: none;
+}
+
+.btn-info.disabled,
+.btn-info[disabled],
+fieldset[disabled] .btn-info,
+.btn-info.disabled:hover,
+.btn-info[disabled]:hover,
+fieldset[disabled] .btn-info:hover,
+.btn-info.disabled:focus,
+.btn-info[disabled]:focus,
+fieldset[disabled] .btn-info:focus,
+.btn-info.disabled:active,
+.btn-info[disabled]:active,
+fieldset[disabled] .btn-info:active,
+.btn-info.disabled.active,
+.btn-info[disabled].active,
+fieldset[disabled] .btn-info.active {
+  background-color: #5bc0de;
+  border-color: #46b8da;
+}
+
+.btn-info .badge {
+  color: #5bc0de;
+  background-color: #fff;
+}
+
+.btn-link {
+  font-weight: normal;
+  color: #428bca;
+  cursor: pointer;
+  border-radius: 0;
+}
+
+.btn-link,
+.btn-link:active,
+.btn-link[disabled],
+fieldset[disabled] .btn-link {
+  background-color: transparent;
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn-link,
+.btn-link:hover,
+.btn-link:focus,
+.btn-link:active {
+  border-color: transparent;
+}
+
+.btn-link:hover,
+.btn-link:focus {
+  color: #2a6496;
+  text-decoration: underline;
+  background-color: transparent;
+}
+
+.btn-link[disabled]:hover,
+fieldset[disabled] .btn-link:hover,
+.btn-link[disabled]:focus,
+fieldset[disabled] .btn-link:focus {
+  color: #999999;
+  text-decoration: none;
+}
+
+.btn-lg {
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+.btn-sm {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-xs {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-block {
+  display: block;
+  width: 100%;
+  padding-right: 0;
+  padding-left: 0;
+}
+
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+
+input[type="submit"].btn-block,
+input[type="reset"].btn-block,
+input[type="button"].btn-block {
+  width: 100%;
+}
+
+.fade {
+  opacity: 0;
+  -webkit-transition: opacity 0.15s linear;
+          transition: opacity 0.15s linear;
+}
+
+.fade.in {
+  opacity: 1;
+}
+
+.collapse {
+  display: none;
+}
+
+.collapse.in {
+  display: block;
+}
+
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  -webkit-transition: height 0.35s ease;
+          transition: height 0.35s ease;
+}
+
+@font-face {
+  font-family: 'Glyphicons Halflings';
+  src: url('../fonts/glyphicons-halflings-regular.eot');
+  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg');
+}
+
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  -webkit-font-smoothing: antialiased;
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.glyphicon:empty {
+  width: 1em;
+}
+
+.glyphicon-asterisk:before {
+  content: "\2a";
+}
+
+.glyphicon-plus:before {
+  content: "\2b";
+}
+
+.glyphicon-euro:before {
+  content: "\20ac";
+}
+
+.glyphicon-minus:before {
+  content: "\2212";
+}
+
+.glyphicon-cloud:before {
+  content: "\2601";
+}
+
+.glyphicon-envelope:before {
+  content: "\2709";
+}
+
+.glyphicon-pencil:before {
+  content: "\270f";
+}
+
+.glyphicon-glass:before {
+  content: "\e001";
+}
+
+.glyphicon-music:before {
+  content: "\e002";
+}
+
+.glyphicon-search:before {
+  content: "\e003";
+}
+
+.glyphicon-heart:before {
+  content: "\e005";
+}
+
+.glyphicon-star:before {
+  content: "\e006";
+}
+
+.glyphicon-star-empty:before {
+  content: "\e007";
+}
+
+.glyphicon-user:before {
+  content: "\e008";
+}
+
+.glyphicon-film:before {
+  content: "\e009";
+}
+
+.glyphicon-th-large:before {
+  content: "\e010";
+}
+
+.glyphicon-th:before {
+  content: "\e011";
+}
+
+.glyphicon-th-list:before {
+  content: "\e012";
+}
+
+.glyphicon-ok:before {
+  content: "\e013";
+}
+
+.glyphicon-remove:before {
+  content: "\e014";
+}
+
+.glyphicon-zoom-in:before {
+  content: "\e015";
+}
+
+.glyphicon-zoom-out:before {
+  content: "\e016";
+}
+
+.glyphicon-off:before {
+  content: "\e017";
+}
+
+.glyphicon-signal:before {
+  content: "\e018";
+}
+
+.glyphicon-cog:before {
+  content: "\e019";
+}
+
+.glyphicon-trash:before {
+  content: "\e020";
+}
+
+.glyphicon-home:before {
+  content: "\e021";
+}
+
+.glyphicon-file:before {
+  content: "\e022";
+}
+
+.glyphicon-time:before {
+  content: "\e023";
+}
+
+.glyphicon-road:before {
+  content: "\e024";
+}
+
+.glyphicon-download-alt:before {
+  content: "\e025";
+}
+
+.glyphicon-download:before {
+  content: "\e026";
+}
+
+.glyphicon-upload:before {
+  content: "\e027";
+}
+
+.glyphicon-inbox:before {
+  content: "\e028";
+}
+
+.glyphicon-play-circle:before {
+  content: "\e029";
+}
+
+.glyphicon-repeat:before {
+  content: "\e030";
+}
+
+.glyphicon-refresh:before {
+  content: "\e031";
+}
+
+.glyphicon-list-alt:before {
+  content: "\e032";
+}
+
+.glyphicon-lock:before {
+  content: "\e033";
+}
+
+.glyphicon-flag:before {
+  content: "\e034";
+}
+
+.glyphicon-headphones:before {
+  content: "\e035";
+}
+
+.glyphicon-volume-off:before {
+  content: "\e036";
+}
+
+.glyphicon-volume-down:before {
+  content: "\e037";
+}
+
+.glyphicon-volume-up:before {
+  content: "\e038";
+}
+
+.glyphicon-qrcode:before {
+  content: "\e039";
+}
+
+.glyphicon-barcode:before {
+  content: "\e040";
+}
+
+.glyphicon-tag:before {
+  content: "\e041";
+}
+
+.glyphicon-tags:before {
+  content: "\e042";
+}
+
+.glyphicon-book:before {
+  content: "\e043";
+}
+
+.glyphicon-bookmark:before {
+  content: "\e044";
+}
+
+.glyphicon-print:before {
+  content: "\e045";
+}
+
+.glyphicon-camera:before {
+  content: "\e046";
+}
+
+.glyphicon-font:before {
+  content: "\e047";
+}
+
+.glyphicon-bold:before {
+  content: "\e048";
+}
+
+.glyphicon-italic:before {
+  content: "\e049";
+}
+
+.glyphicon-text-height:before {
+  content: "\e050";
+}
+
+.glyphicon-text-width:before {
+  content: "\e051";
+}
+
+.glyphicon-align-left:before {
+  content: "\e052";
+}
+
+.glyphicon-align-center:before {
+  content: "\e053";
+}
+
+.glyphicon-align-right:before {
+  content: "\e054";
+}
+
+.glyphicon-align-justify:before {
+  content: "\e055";
+}
+
+.glyphicon-list:before {
+  content: "\e056";
+}
+
+.glyphicon-indent-left:before {
+  content: "\e057";
+}
+
+.glyphicon-indent-right:before {
+  content: "\e058";
+}
+
+.glyphicon-facetime-video:before {
+  content: "\e059";
+}
+
+.glyphicon-picture:before {
+  content: "\e060";
+}
+
+.glyphicon-map-marker:before {
+  content: "\e062";
+}
+
+.glyphicon-adjust:before {
+  content: "\e063";
+}
+
+.glyphicon-tint:before {
+  content: "\e064";
+}
+
+.glyphicon-edit:before {
+  content: "\e065";
+}
+
+.glyphicon-share:before {
+  content: "\e066";
+}
+
+.glyphicon-check:before {
+  content: "\e067";
+}
+
+.glyphicon-move:before {
+  content: "\e068";
+}
+
+.glyphicon-step-backward:before {
+  content: "\e069";
+}
+
+.glyphicon-fast-backward:before {
+  content: "\e070";
+}
+
+.glyphicon-backward:before {
+  content: "\e071";
+}
+
+.glyphicon-play:before {
+  content: "\e072";
+}
+
+.glyphicon-pause:before {
+  content: "\e073";
+}
+
+.glyphicon-stop:before {
+  content: "\e074";
+}
+
+.glyphicon-forward:before {
+  content: "\e075";
+}
+
+.glyphicon-fast-forward:before {
+  content: "\e076";
+}
+
+.glyphicon-step-forward:before {
+  content: "\e077";
+}
+
+.glyphicon-eject:before {
+  content: "\e078";
+}
+
+.glyphicon-chevron-left:before {
+  content: "\e079";
+}
+
+.glyphicon-chevron-right:before {
+  content: "\e080";
+}
+
+.glyphicon-plus-sign:before {
+  content: "\e081";
+}
+
+.glyphicon-minus-sign:before {
+  content: "\e082";
+}
+
+.glyphicon-remove-sign:before {
+  content: "\e083";
+}
+
+.glyphicon-ok-sign:before {
+  content: "\e084";
+}
+
+.glyphicon-question-sign:before {
+  content: "\e085";
+}
+
+.glyphicon-info-sign:before {
+  content: "\e086";
+}
+
+.glyphicon-screenshot:before {
+  content: "\e087";
+}
+
+.glyphicon-remove-circle:before {
+  content: "\e088";
+}
+
+.glyphicon-ok-circle:before {
+  content: "\e089";
+}
+
+.glyphicon-ban-circle:before {
+  content: "\e090";
+}
+
+.glyphicon-arrow-left:before {
+  content: "\e091";
+}
+
+.glyphicon-arrow-right:before {
+  content: "\e092";
+}
+
+.glyphicon-arrow-up:before {
+  content: "\e093";
+}
+
+.glyphicon-arrow-down:before {
+  content: "\e094";
+}
+
+.glyphicon-share-alt:before {
+  content: "\e095";
+}
+
+.glyphicon-resize-full:before {
+  content: "\e096";
+}
+
+.glyphicon-resize-small:before {
+  content: "\e097";
+}
+
+.glyphicon-exclamation-sign:before {
+  content: "\e101";
+}
+
+.glyphicon-gift:before {
+  content: "\e102";
+}
+
+.glyphicon-leaf:before {
+  content: "\e103";
+}
+
+.glyphicon-fire:before {
+  content: "\e104";
+}
+
+.glyphicon-eye-open:before {
+  content: "\e105";
+}
+
+.glyphicon-eye-close:before {
+  content: "\e106";
+}
+
+.glyphicon-warning-sign:before {
+  content: "\e107";
+}
+
+.glyphicon-plane:before {
+  content: "\e108";
+}
+
+.glyphicon-calendar:before {
+  content: "\e109";
+}
+
+.glyphicon-random:before {
+  content: "\e110";
+}
+
+.glyphicon-comment:before {
+  content: "\e111";
+}
+
+.glyphicon-magnet:before {
+  content: "\e112";
+}
+
+.glyphicon-chevron-up:before {
+  content: "\e113";
+}
+
+.glyphicon-chevron-down:before {
+  content: "\e114";
+}
+
+.glyphicon-retweet:before {
+  content: "\e115";
+}
+
+.glyphicon-shopping-cart:before {
+  content: "\e116";
+}
+
+.glyphicon-folder-close:before {
+  content: "\e117";
+}
+
+.glyphicon-folder-open:before {
+  content: "\e118";
+}
+
+.glyphicon-resize-vertical:before {
+  content: "\e119";
+}
+
+.glyphicon-resize-horizontal:before {
+  content: "\e120";
+}
+
+.glyphicon-hdd:before {
+  content: "\e121";
+}
+
+.glyphicon-bullhorn:before {
+  content: "\e122";
+}
+
+.glyphicon-bell:before {
+  content: "\e123";
+}
+
+.glyphicon-certificate:before {
+  content: "\e124";
+}
+
+.glyphicon-thumbs-up:before {
+  content: "\e125";
+}
+
+.glyphicon-thumbs-down:before {
+  content: "\e126";
+}
+
+.glyphicon-hand-right:before {
+  content: "\e127";
+}
+
+.glyphicon-hand-left:before {
+  content: "\e128";
+}
+
+.glyphicon-hand-up:before {
+  content: "\e129";
+}
+
+.glyphicon-hand-down:before {
+  content: "\e130";
+}
+
+.glyphicon-circle-arrow-right:before {
+  content: "\e131";
+}
+
+.glyphicon-circle-arrow-left:before {
+  content: "\e132";
+}
+
+.glyphicon-circle-arrow-up:before {
+  content: "\e133";
+}
+
+.glyphicon-circle-arrow-down:before {
+  content: "\e134";
+}
+
+.glyphicon-globe:before {
+  content: "\e135";
+}
+
+.glyphicon-wrench:before {
+  content: "\e136";
+}
+
+.glyphicon-tasks:before {
+  content: "\e137";
+}
+
+.glyphicon-filter:before {
+  content: "\e138";
+}
+
+.glyphicon-briefcase:before {
+  content: "\e139";
+}
+
+.glyphicon-fullscreen:before {
+  content: "\e140";
+}
+
+.glyphicon-dashboard:before {
+  content: "\e141";
+}
+
+.glyphicon-paperclip:before {
+  content: "\e142";
+}
+
+.glyphicon-heart-empty:before {
+  content: "\e143";
+}
+
+.glyphicon-link:before {
+  content: "\e144";
+}
+
+.glyphicon-phone:before {
+  content: "\e145";
+}
+
+.glyphicon-pushpin:before {
+  content: "\e146";
+}
+
+.glyphicon-usd:before {
+  content: "\e148";
+}
+
+.glyphicon-gbp:before {
+  content: "\e149";
+}
+
+.glyphicon-sort:before {
+  content: "\e150";
+}
+
+.glyphicon-sort-by-alphabet:before {
+  content: "\e151";
+}
+
+.glyphicon-sort-by-alphabet-alt:before {
+  content: "\e152";
+}
+
+.glyphicon-sort-by-order:before {
+  content: "\e153";
+}
+
+.glyphicon-sort-by-order-alt:before {
+  content: "\e154";
+}
+
+.glyphicon-sort-by-attributes:before {
+  content: "\e155";
+}
+
+.glyphicon-sort-by-attributes-alt:before {
+  content: "\e156";
+}
+
+.glyphicon-unchecked:before {
+  content: "\e157";
+}
+
+.glyphicon-expand:before {
+  content: "\e158";
+}
+
+.glyphicon-collapse-down:before {
+  content: "\e159";
+}
+
+.glyphicon-collapse-up:before {
+  content: "\e160";
+}
+
+.glyphicon-log-in:before {
+  content: "\e161";
+}
+
+.glyphicon-flash:before {
+  content: "\e162";
+}
+
+.glyphicon-log-out:before {
+  content: "\e163";
+}
+
+.glyphicon-new-window:before {
+  content: "\e164";
+}
+
+.glyphicon-record:before {
+  content: "\e165";
+}
+
+.glyphicon-save:before {
+  content: "\e166";
+}
+
+.glyphicon-open:before {
+  content: "\e167";
+}
+
+.glyphicon-saved:before {
+  content: "\e168";
+}
+
+.glyphicon-import:before {
+  content: "\e169";
+}
+
+.glyphicon-export:before {
+  content: "\e170";
+}
+
+.glyphicon-send:before {
+  content: "\e171";
+}
+
+.glyphicon-floppy-disk:before {
+  content: "\e172";
+}
+
+.glyphicon-floppy-saved:before {
+  content: "\e173";
+}
+
+.glyphicon-floppy-remove:before {
+  content: "\e174";
+}
+
+.glyphicon-floppy-save:before {
+  content: "\e175";
+}
+
+.glyphicon-floppy-open:before {
+  content: "\e176";
+}
+
+.glyphicon-credit-card:before {
+  content: "\e177";
+}
+
+.glyphicon-transfer:before {
+  content: "\e178";
+}
+
+.glyphicon-cutlery:before {
+  content: "\e179";
+}
+
+.glyphicon-header:before {
+  content: "\e180";
+}
+
+.glyphicon-compressed:before {
+  content: "\e181";
+}
+
+.glyphicon-earphone:before {
+  content: "\e182";
+}
+
+.glyphicon-phone-alt:before {
+  content: "\e183";
+}
+
+.glyphicon-tower:before {
+  content: "\e184";
+}
+
+.glyphicon-stats:before {
+  content: "\e185";
+}
+
+.glyphicon-sd-video:before {
+  content: "\e186";
+}
+
+.glyphicon-hd-video:before {
+  content: "\e187";
+}
+
+.glyphicon-subtitles:before {
+  content: "\e188";
+}
+
+.glyphicon-sound-stereo:before {
+  content: "\e189";
+}
+
+.glyphicon-sound-dolby:before {
+  content: "\e190";
+}
+
+.glyphicon-sound-5-1:before {
+  content: "\e191";
+}
+
+.glyphicon-sound-6-1:before {
+  content: "\e192";
+}
+
+.glyphicon-sound-7-1:before {
+  content: "\e193";
+}
+
+.glyphicon-copyright-mark:before {
+  content: "\e194";
+}
+
+.glyphicon-registration-mark:before {
+  content: "\e195";
+}
+
+.glyphicon-cloud-download:before {
+  content: "\e197";
+}
+
+.glyphicon-cloud-upload:before {
+  content: "\e198";
+}
+
+.glyphicon-tree-conifer:before {
+  content: "\e199";
+}
+
+.glyphicon-tree-deciduous:before {
+  content: "\e200";
+}
+
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top: 4px solid;
+  border-right: 4px solid transparent;
+  border-left: 4px solid transparent;
+}
+
+.dropdown {
+  position: relative;
+}
+
+.dropdown-toggle:focus {
+  outline: 0;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0;
+  font-size: 14px;
+  list-style: none;
+  background-color: #ffffff;
+  border: 1px solid #cccccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+          box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+  background-clip: padding-box;
+}
+
+.dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+
+.dropdown-menu .divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+
+.dropdown-menu > li > a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: normal;
+  line-height: 1.428571429;
+  color: #333333;
+  white-space: nowrap;
+}
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  color: #262626;
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  color: #ffffff;
+  text-decoration: none;
+  background-color: #428bca;
+  outline: 0;
+}
+
+.dropdown-menu > .disabled > a,
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  color: #999999;
+}
+
+.dropdown-menu > .disabled > a:hover,
+.dropdown-menu > .disabled > a:focus {
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+
+.open > .dropdown-menu {
+  display: block;
+}
+
+.open > a {
+  outline: 0;
+}
+
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: 12px;
+  line-height: 1.428571429;
+  color: #999999;
+}
+
+.dropdown-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 990;
+}
+
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+  border-top: 0;
+  border-bottom: 4px solid;
+  content: "";
+}
+
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+  top: auto;
+  bottom: 100%;
+  margin-bottom: 1px;
+}
+
+@media (min-width: 768px) {
+  .navbar-right .dropdown-menu {
+    right: 0;
+    left: auto;
+  }
+}
+
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+  position: relative;
+  float: left;
+}
+
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover,
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus,
+.btn-group > .btn:active,
+.btn-group-vertical > .btn:active,
+.btn-group > .btn.active,
+.btn-group-vertical > .btn.active {
+  z-index: 2;
+}
+
+.btn-group > .btn:focus,
+.btn-group-vertical > .btn:focus {
+  outline: none;
+}
+
+.btn-group .btn + .btn,
+.btn-group .btn + .btn-group,
+.btn-group .btn-group + .btn,
+.btn-group .btn-group + .btn-group {
+  margin-left: -1px;
+}
+
+.btn-toolbar:before,
+.btn-toolbar:after {
+  display: table;
+  content: " ";
+}
+
+.btn-toolbar:after {
+  clear: both;
+}
+
+.btn-toolbar:before,
+.btn-toolbar:after {
+  display: table;
+  content: " ";
+}
+
+.btn-toolbar:after {
+  clear: both;
+}
+
+.btn-toolbar .btn-group {
+  float: left;
+}
+
+.btn-toolbar > .btn + .btn,
+.btn-toolbar > .btn-group + .btn,
+.btn-toolbar > .btn + .btn-group,
+.btn-toolbar > .btn-group + .btn-group {
+  margin-left: 5px;
+}
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+.btn-group > .btn:first-child {
+  margin-left: 0;
+}
+
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group > .btn-group {
+  float: left;
+}
+
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group > .btn-group:first-child > .btn:last-child,
+.btn-group > .btn-group:first-child > .dropdown-toggle {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn-group:last-child > .btn:first-child {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+.btn-group-xs > .btn {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-group-sm > .btn {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+.btn-group-lg > .btn {
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+.btn-group > .btn + .dropdown-toggle {
+  padding-right: 8px;
+  padding-left: 8px;
+}
+
+.btn-group > .btn-lg + .dropdown-toggle {
+  padding-right: 12px;
+  padding-left: 12px;
+}
+
+.btn-group.open .dropdown-toggle {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.btn-group.open .dropdown-toggle.btn-link {
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+
+.btn .caret {
+  margin-left: 0;
+}
+
+.btn-lg .caret {
+  border-width: 5px 5px 0;
+  border-bottom-width: 0;
+}
+
+.dropup .btn-lg .caret {
+  border-width: 0 5px 5px;
+}
+
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group,
+.btn-group-vertical > .btn-group > .btn {
+  display: block;
+  float: none;
+  width: 100%;
+  max-width: 100%;
+}
+
+.btn-group-vertical > .btn-group:before,
+.btn-group-vertical > .btn-group:after {
+  display: table;
+  content: " ";
+}
+
+.btn-group-vertical > .btn-group:after {
+  clear: both;
+}
+
+.btn-group-vertical > .btn-group:before,
+.btn-group-vertical > .btn-group:after {
+  display: table;
+  content: " ";
+}
+
+.btn-group-vertical > .btn-group:after {
+  clear: both;
+}
+
+.btn-group-vertical > .btn-group > .btn {
+  float: none;
+}
+
+.btn-group-vertical > .btn + .btn,
+.btn-group-vertical > .btn + .btn-group,
+.btn-group-vertical > .btn-group + .btn,
+.btn-group-vertical > .btn-group + .btn-group {
+  margin-top: -1px;
+  margin-left: 0;
+}
+
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+  border-top-right-radius: 0;
+  border-bottom-left-radius: 4px;
+  border-top-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:first-child > .btn:last-child,
+.btn-group-vertical > .btn-group:first-child > .dropdown-toggle {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn-group:last-child > .btn:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  border-collapse: separate;
+  table-layout: fixed;
+}
+
+.btn-group-justified > .btn,
+.btn-group-justified > .btn-group {
+  display: table-cell;
+  float: none;
+  width: 1%;
+}
+
+.btn-group-justified > .btn-group .btn {
+  width: 100%;
+}
+
+[data-toggle="buttons"] > .btn > input[type="radio"],
+[data-toggle="buttons"] > .btn > input[type="checkbox"] {
+  display: none;
+}
+
+.input-group {
+  position: relative;
+  display: table;
+  border-collapse: separate;
+}
+
+.input-group[class*="col-"] {
+  float: none;
+  padding-right: 0;
+  padding-left: 0;
+}
+
+.input-group .form-control {
+  width: 100%;
+  margin-bottom: 0;
+}
+
+.input-group-lg > .form-control,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  padding: 10px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+  border-radius: 6px;
+}
+
+select.input-group-lg > .form-control,
+select.input-group-lg > .input-group-addon,
+select.input-group-lg > .input-group-btn > .btn {
+  height: 46px;
+  line-height: 46px;
+}
+
+textarea.input-group-lg > .form-control,
+textarea.input-group-lg > .input-group-addon,
+textarea.input-group-lg > .input-group-btn > .btn {
+  height: auto;
+}
+
+.input-group-sm > .form-control,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+}
+
+select.input-group-sm > .form-control,
+select.input-group-sm > .input-group-addon,
+select.input-group-sm > .input-group-btn > .btn {
+  height: 30px;
+  line-height: 30px;
+}
+
+textarea.input-group-sm > .form-control,
+textarea.input-group-sm > .input-group-addon,
+textarea.input-group-sm > .input-group-btn > .btn {
+  height: auto;
+}
+
+.input-group-addon,
+.input-group-btn,
+.input-group .form-control {
+  display: table-cell;
+}
+
+.input-group-addon:not(:first-child):not(:last-child),
+.input-group-btn:not(:first-child):not(:last-child),
+.input-group .form-control:not(:first-child):not(:last-child) {
+  border-radius: 0;
+}
+
+.input-group-addon,
+.input-group-btn {
+  width: 1%;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+.input-group-addon {
+  padding: 6px 12px;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 1;
+  color: #555555;
+  text-align: center;
+  background-color: #eeeeee;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+}
+
+.input-group-addon.input-sm {
+  padding: 5px 10px;
+  font-size: 12px;
+  border-radius: 3px;
+}
+
+.input-group-addon.input-lg {
+  padding: 10px 16px;
+  font-size: 18px;
+  border-radius: 6px;
+}
+
+.input-group-addon input[type="radio"],
+.input-group-addon input[type="checkbox"] {
+  margin-top: 0;
+}
+
+.input-group .form-control:first-child,
+.input-group-addon:first-child,
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .dropdown-toggle,
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.input-group-addon:first-child {
+  border-right: 0;
+}
+
+.input-group .form-control:last-child,
+.input-group-addon:last-child,
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .dropdown-toggle,
+.input-group-btn:first-child > .btn:not(:first-child) {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.input-group-addon:last-child {
+  border-left: 0;
+}
+
+.input-group-btn {
+  position: relative;
+  white-space: nowrap;
+}
+
+.input-group-btn:first-child > .btn {
+  margin-right: -1px;
+}
+
+.input-group-btn:last-child > .btn {
+  margin-left: -1px;
+}
+
+.input-group-btn > .btn {
+  position: relative;
+}
+
+.input-group-btn > .btn + .btn {
+  margin-left: -4px;
+}
+
+.input-group-btn > .btn:hover,
+.input-group-btn > .btn:active {
+  z-index: 2;
+}
+
+.nav {
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+
+.nav:before,
+.nav:after {
+  display: table;
+  content: " ";
+}
+
+.nav:after {
+  clear: both;
+}
+
+.nav:before,
+.nav:after {
+  display: table;
+  content: " ";
+}
+
+.nav:after {
+  clear: both;
+}
+
+.nav > li {
+  position: relative;
+  display: block;
+}
+
+.nav > li > a {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+}
+
+.nav > li > a:hover,
+.nav > li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+
+.nav > li.disabled > a {
+  color: #999999;
+}
+
+.nav > li.disabled > a:hover,
+.nav > li.disabled > a:focus {
+  color: #999999;
+  text-decoration: none;
+  cursor: not-allowed;
+  background-color: transparent;
+}
+
+.nav .open > a,
+.nav .open > a:hover,
+.nav .open > a:focus {
+  background-color: #eeeeee;
+  border-color: #428bca;
+}
+
+.nav .nav-divider {
+  height: 1px;
+  margin: 9px 0;
+  overflow: hidden;
+  background-color: #e5e5e5;
+}
+
+.nav > li > a > img {
+  max-width: none;
+}
+
+.nav-tabs {
+  border-bottom: 1px solid #dddddd;
+}
+
+.nav-tabs > li {
+  float: left;
+  margin-bottom: -1px;
+}
+
+.nav-tabs > li > a {
+  margin-right: 2px;
+  line-height: 1.428571429;
+  border: 1px solid transparent;
+  border-radius: 4px 4px 0 0;
+}
+
+.nav-tabs > li > a:hover {
+  border-color: #eeeeee #eeeeee #dddddd;
+}
+
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:hover,
+.nav-tabs > li.active > a:focus {
+  color: #555555;
+  cursor: default;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-bottom-color: transparent;
+}
+
+.nav-tabs.nav-justified {
+  width: 100%;
+  border-bottom: 0;
+}
+
+.nav-tabs.nav-justified > li {
+  float: none;
+}
+
+.nav-tabs.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+
+.nav-tabs.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-tabs.nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+
+.nav-tabs.nav-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+
+.nav-tabs.nav-justified > .active > a,
+.nav-tabs.nav-justified > .active > a:hover,
+.nav-tabs.nav-justified > .active > a:focus {
+  border: 1px solid #dddddd;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs.nav-justified > li > a {
+    border-bottom: 1px solid #dddddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs.nav-justified > .active > a,
+  .nav-tabs.nav-justified > .active > a:hover,
+  .nav-tabs.nav-justified > .active > a:focus {
+    border-bottom-color: #ffffff;
+  }
+}
+
+.nav-pills > li {
+  float: left;
+}
+
+.nav-pills > li > a {
+  border-radius: 4px;
+}
+
+.nav-pills > li + li {
+  margin-left: 2px;
+}
+
+.nav-pills > li.active > a,
+.nav-pills > li.active > a:hover,
+.nav-pills > li.active > a:focus {
+  color: #ffffff;
+  background-color: #428bca;
+}
+
+.nav-stacked > li {
+  float: none;
+}
+
+.nav-stacked > li + li {
+  margin-top: 2px;
+  margin-left: 0;
+}
+
+.nav-justified {
+  width: 100%;
+}
+
+.nav-justified > li {
+  float: none;
+}
+
+.nav-justified > li > a {
+  margin-bottom: 5px;
+  text-align: center;
+}
+
+.nav-justified > .dropdown .dropdown-menu {
+  top: auto;
+  left: auto;
+}
+
+@media (min-width: 768px) {
+  .nav-justified > li {
+    display: table-cell;
+    width: 1%;
+  }
+  .nav-justified > li > a {
+    margin-bottom: 0;
+  }
+}
+
+.nav-tabs-justified {
+  border-bottom: 0;
+}
+
+.nav-tabs-justified > li > a {
+  margin-right: 0;
+  border-radius: 4px;
+}
+
+.nav-tabs-justified > .active > a,
+.nav-tabs-justified > .active > a:hover,
+.nav-tabs-justified > .active > a:focus {
+  border: 1px solid #dddddd;
+}
+
+@media (min-width: 768px) {
+  .nav-tabs-justified > li > a {
+    border-bottom: 1px solid #dddddd;
+    border-radius: 4px 4px 0 0;
+  }
+  .nav-tabs-justified > .active > a,
+  .nav-tabs-justified > .active > a:hover,
+  .nav-tabs-justified > .active > a:focus {
+    border-bottom-color: #ffffff;
+  }
+}
+
+.tab-content > .tab-pane {
+  display: none;
+}
+
+.tab-content > .active {
+  display: block;
+}
+
+.nav-tabs .dropdown-menu {
+  margin-top: -1px;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar {
+  position: relative;
+  min-height: 50px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+}
+
+.navbar:before,
+.navbar:after {
+  display: table;
+  content: " ";
+}
+
+.navbar:after {
+  clear: both;
+}
+
+.navbar:before,
+.navbar:after {
+  display: table;
+  content: " ";
+}
+
+.navbar:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .navbar {
+    border-radius: 4px;
+  }
+}
+
+.navbar-header:before,
+.navbar-header:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-header:after {
+  clear: both;
+}
+
+.navbar-header:before,
+.navbar-header:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-header:after {
+  clear: both;
+}
+
+@media (min-width: 768px) {
+  .navbar-header {
+    float: left;
+  }
+}
+
+.navbar-collapse {
+  max-height: 340px;
+  padding-right: 15px;
+  padding-left: 15px;
+  overflow-x: visible;
+  border-top: 1px solid transparent;
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+  -webkit-overflow-scrolling: touch;
+}
+
+.navbar-collapse:before,
+.navbar-collapse:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-collapse:after {
+  clear: both;
+}
+
+.navbar-collapse:before,
+.navbar-collapse:after {
+  display: table;
+  content: " ";
+}
+
+.navbar-collapse:after {
+  clear: both;
+}
+
+.navbar-collapse.in {
+  overflow-y: auto;
+}
+
+@media (min-width: 768px) {
+  .navbar-collapse {
+    width: auto;
+    border-top: 0;
+    box-shadow: none;
+  }
+  .navbar-collapse.collapse {
+    display: block !important;
+    height: auto !important;
+    padding-bottom: 0;
+    overflow: visible !important;
+  }
+  .navbar-collapse.in {
+    overflow-y: visible;
+  }
+  .navbar-fixed-top .navbar-collapse,
+  .navbar-static-top .navbar-collapse,
+  .navbar-fixed-bottom .navbar-collapse {
+    padding-right: 0;
+    padding-left: 0;
+  }
+}
+
+.container > .navbar-header,
+.container > .navbar-collapse {
+  margin-right: -15px;
+  margin-left: -15px;
+}
+
+@media (min-width: 768px) {
+  .container > .navbar-header,
+  .container > .navbar-collapse {
+    margin-right: 0;
+    margin-left: 0;
+  }
+}
+
+.navbar-static-top {
+  z-index: 1000;
+  border-width: 0 0 1px;
+}
+
+@media (min-width: 768px) {
+  .navbar-static-top {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  position: fixed;
+  right: 0;
+  left: 0;
+  z-index: 1030;
+}
+
+@media (min-width: 768px) {
+  .navbar-fixed-top,
+  .navbar-fixed-bottom {
+    border-radius: 0;
+  }
+}
+
+.navbar-fixed-top {
+  top: 0;
+  border-width: 0 0 1px;
+}
+
+.navbar-fixed-bottom {
+  bottom: 0;
+  margin-bottom: 0;
+  border-width: 1px 0 0;
+}
+
+.navbar-brand {
+  float: left;
+  padding: 15px 15px;
+  font-size: 18px;
+  line-height: 20px;
+}
+
+.navbar-brand:hover,
+.navbar-brand:focus {
+  text-decoration: none;
+}
+
+@media (min-width: 768px) {
+  .navbar > .container .navbar-brand {
+    margin-left: -15px;
+  }
+}
+
+.navbar-toggle {
+  position: relative;
+  float: right;
+  padding: 9px 10px;
+  margin-top: 8px;
+  margin-right: 15px;
+  margin-bottom: 8px;
+  background-color: transparent;
+  background-image: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+
+.navbar-toggle .icon-bar {
+  display: block;
+  width: 22px;
+  height: 2px;
+  border-radius: 1px;
+}
+
+.navbar-toggle .icon-bar + .icon-bar {
+  margin-top: 4px;
+}
+
+@media (min-width: 768px) {
+  .navbar-toggle {
+    display: none;
+  }
+}
+
+.navbar-nav {
+  margin: 7.5px -15px;
+}
+
+.navbar-nav > li > a {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  line-height: 20px;
+}
+
+@media (max-width: 767px) {
+  .navbar-nav .open .dropdown-menu {
+    position: static;
+    float: none;
+    width: auto;
+    margin-top: 0;
+    background-color: transparent;
+    border: 0;
+    box-shadow: none;
+  }
+  .navbar-nav .open .dropdown-menu > li > a,
+  .navbar-nav .open .dropdown-menu .dropdown-header {
+    padding: 5px 15px 5px 25px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a {
+    line-height: 20px;
+  }
+  .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-nav .open .dropdown-menu > li > a:focus {
+    background-image: none;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-nav {
+    float: left;
+    margin: 0;
+  }
+  .navbar-nav > li {
+    float: left;
+  }
+  .navbar-nav > li > a {
+    padding-top: 15px;
+    padding-bottom: 15px;
+  }
+  .navbar-nav.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-left {
+    float: left !important;
+  }
+  .navbar-right {
+    float: right !important;
+  }
+}
+
+.navbar-form {
+  padding: 10px 15px;
+  margin-top: 8px;
+  margin-right: -15px;
+  margin-bottom: 8px;
+  margin-left: -15px;
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+}
+
+@media (min-width: 768px) {
+  .navbar-form .form-group {
+    display: inline-block;
+    margin-bottom: 0;
+    vertical-align: middle;
+  }
+  .navbar-form .form-control {
+    display: inline-block;
+  }
+  .navbar-form select.form-control {
+    width: auto;
+  }
+  .navbar-form .radio,
+  .navbar-form .checkbox {
+    display: inline-block;
+    padding-left: 0;
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+  .navbar-form .radio input[type="radio"],
+  .navbar-form .checkbox input[type="checkbox"] {
+    float: none;
+    margin-left: 0;
+  }
+}
+
+@media (max-width: 767px) {
+  .navbar-form .form-group {
+    margin-bottom: 5px;
+  }
+}
+
+@media (min-width: 768px) {
+  .navbar-form {
+    width: auto;
+    padding-top: 0;
+    padding-bottom: 0;
+    margin-right: 0;
+    margin-left: 0;
+    border: 0;
+    -webkit-box-shadow: none;
+            box-shadow: none;
+  }
+  .navbar-form.navbar-right:last-child {
+    margin-right: -15px;
+  }
+}
+
+.navbar-nav > li > .dropdown-menu {
+  margin-top: 0;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.navbar-nav.pull-right > li > .dropdown-menu,
+.navbar-nav > li > .dropdown-menu.pull-right {
+  right: 0;
+  left: auto;
+}
+
+.navbar-btn {
+  margin-top: 8px;
+  margin-bottom: 8px;
+}
+
+.navbar-btn.btn-sm {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+
+.navbar-btn.btn-xs {
+  margin-top: 14px;
+  margin-bottom: 14px;
+}
+
+.navbar-text {
+  margin-top: 15px;
+  margin-bottom: 15px;
+}
+
+@media (min-width: 768px) {
+  .navbar-text {
+    float: left;
+    margin-right: 15px;
+    margin-left: 15px;
+  }
+  .navbar-text.navbar-right:last-child {
+    margin-right: 0;
+  }
+}
+
+.navbar-default {
+  background-color: #f8f8f8;
+  border-color: #e7e7e7;
+}
+
+.navbar-default .navbar-brand {
+  color: #777777;
+}
+
+.navbar-default .navbar-brand:hover,
+.navbar-default .navbar-brand:focus {
+  color: #5e5e5e;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-text {
+  color: #777777;
+}
+
+.navbar-default .navbar-nav > li > a {
+  color: #777777;
+}
+
+.navbar-default .navbar-nav > li > a:hover,
+.navbar-default .navbar-nav > li > a:focus {
+  color: #333333;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-nav > .active > a,
+.navbar-default .navbar-nav > .active > a:hover,
+.navbar-default .navbar-nav > .active > a:focus {
+  color: #555555;
+  background-color: #e7e7e7;
+}
+
+.navbar-default .navbar-nav > .disabled > a,
+.navbar-default .navbar-nav > .disabled > a:hover,
+.navbar-default .navbar-nav > .disabled > a:focus {
+  color: #cccccc;
+  background-color: transparent;
+}
+
+.navbar-default .navbar-toggle {
+  border-color: #dddddd;
+}
+
+.navbar-default .navbar-toggle:hover,
+.navbar-default .navbar-toggle:focus {
+  background-color: #dddddd;
+}
+
+.navbar-default .navbar-toggle .icon-bar {
+  background-color: #cccccc;
+}
+
+.navbar-default .navbar-collapse,
+.navbar-default .navbar-form {
+  border-color: #e7e7e7;
+}
+
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .open > a:hover,
+.navbar-default .navbar-nav > .open > a:focus {
+  color: #555555;
+  background-color: #e7e7e7;
+}
+
+@media (max-width: 767px) {
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+    color: #777777;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #333333;
+    background-color: transparent;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #555555;
+    background-color: #e7e7e7;
+  }
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #cccccc;
+    background-color: transparent;
+  }
+}
+
+.navbar-default .navbar-link {
+  color: #777777;
+}
+
+.navbar-default .navbar-link:hover {
+  color: #333333;
+}
+
+.navbar-inverse {
+  background-color: #222222;
+  border-color: #080808;
+}
+
+.navbar-inverse .navbar-brand {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-brand:hover,
+.navbar-inverse .navbar-brand:focus {
+  color: #ffffff;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-text {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-nav > li > a {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-nav > li > a:hover,
+.navbar-inverse .navbar-nav > li > a:focus {
+  color: #ffffff;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-nav > .active > a,
+.navbar-inverse .navbar-nav > .active > a:hover,
+.navbar-inverse .navbar-nav > .active > a:focus {
+  color: #ffffff;
+  background-color: #080808;
+}
+
+.navbar-inverse .navbar-nav > .disabled > a,
+.navbar-inverse .navbar-nav > .disabled > a:hover,
+.navbar-inverse .navbar-nav > .disabled > a:focus {
+  color: #444444;
+  background-color: transparent;
+}
+
+.navbar-inverse .navbar-toggle {
+  border-color: #333333;
+}
+
+.navbar-inverse .navbar-toggle:hover,
+.navbar-inverse .navbar-toggle:focus {
+  background-color: #333333;
+}
+
+.navbar-inverse .navbar-toggle .icon-bar {
+  background-color: #ffffff;
+}
+
+.navbar-inverse .navbar-collapse,
+.navbar-inverse .navbar-form {
+  border-color: #101010;
+}
+
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .open > a:hover,
+.navbar-inverse .navbar-nav > .open > a:focus {
+  color: #ffffff;
+  background-color: #080808;
+}
+
+@media (max-width: 767px) {
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+    border-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+    color: #999999;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+    color: #ffffff;
+    background-color: transparent;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #ffffff;
+    background-color: #080808;
+  }
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+    color: #444444;
+    background-color: transparent;
+  }
+}
+
+.navbar-inverse .navbar-link {
+  color: #999999;
+}
+
+.navbar-inverse .navbar-link:hover {
+  color: #ffffff;
+}
+
+.breadcrumb {
+  padding: 8px 15px;
+  margin-bottom: 20px;
+  list-style: none;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+}
+
+.breadcrumb > li {
+  display: inline-block;
+}
+
+.breadcrumb > li + li:before {
+  padding: 0 5px;
+  color: #cccccc;
+  content: "/\00a0";
+}
+
+.breadcrumb > .active {
+  color: #999999;
+}
+
+.pagination {
+  display: inline-block;
+  padding-left: 0;
+  margin: 20px 0;
+  border-radius: 4px;
+}
+
+.pagination > li {
+  display: inline;
+}
+
+.pagination > li > a,
+.pagination > li > span {
+  position: relative;
+  float: left;
+  padding: 6px 12px;
+  margin-left: -1px;
+  line-height: 1.428571429;
+  text-decoration: none;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+}
+
+.pagination > li:first-child > a,
+.pagination > li:first-child > span {
+  margin-left: 0;
+  border-bottom-left-radius: 4px;
+  border-top-left-radius: 4px;
+}
+
+.pagination > li:last-child > a,
+.pagination > li:last-child > span {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+
+.pagination > li > a:hover,
+.pagination > li > span:hover,
+.pagination > li > a:focus,
+.pagination > li > span:focus {
+  background-color: #eeeeee;
+}
+
+.pagination > .active > a,
+.pagination > .active > span,
+.pagination > .active > a:hover,
+.pagination > .active > span:hover,
+.pagination > .active > a:focus,
+.pagination > .active > span:focus {
+  z-index: 2;
+  color: #ffffff;
+  cursor: default;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+.pagination > .disabled > span,
+.pagination > .disabled > span:hover,
+.pagination > .disabled > span:focus,
+.pagination > .disabled > a,
+.pagination > .disabled > a:hover,
+.pagination > .disabled > a:focus {
+  color: #999999;
+  cursor: not-allowed;
+  background-color: #ffffff;
+  border-color: #dddddd;
+}
+
+.pagination-lg > li > a,
+.pagination-lg > li > span {
+  padding: 10px 16px;
+  font-size: 18px;
+}
+
+.pagination-lg > li:first-child > a,
+.pagination-lg > li:first-child > span {
+  border-bottom-left-radius: 6px;
+  border-top-left-radius: 6px;
+}
+
+.pagination-lg > li:last-child > a,
+.pagination-lg > li:last-child > span {
+  border-top-right-radius: 6px;
+  border-bottom-right-radius: 6px;
+}
+
+.pagination-sm > li > a,
+.pagination-sm > li > span {
+  padding: 5px 10px;
+  font-size: 12px;
+}
+
+.pagination-sm > li:first-child > a,
+.pagination-sm > li:first-child > span {
+  border-bottom-left-radius: 3px;
+  border-top-left-radius: 3px;
+}
+
+.pagination-sm > li:last-child > a,
+.pagination-sm > li:last-child > span {
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+
+.pager {
+  padding-left: 0;
+  margin: 20px 0;
+  text-align: center;
+  list-style: none;
+}
+
+.pager:before,
+.pager:after {
+  display: table;
+  content: " ";
+}
+
+.pager:after {
+  clear: both;
+}
+
+.pager:before,
+.pager:after {
+  display: table;
+  content: " ";
+}
+
+.pager:after {
+  clear: both;
+}
+
+.pager li {
+  display: inline;
+}
+
+.pager li > a,
+.pager li > span {
+  display: inline-block;
+  padding: 5px 14px;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 15px;
+}
+
+.pager li > a:hover,
+.pager li > a:focus {
+  text-decoration: none;
+  background-color: #eeeeee;
+}
+
+.pager .next > a,
+.pager .next > span {
+  float: right;
+}
+
+.pager .previous > a,
+.pager .previous > span {
+  float: left;
+}
+
+.pager .disabled > a,
+.pager .disabled > a:hover,
+.pager .disabled > a:focus,
+.pager .disabled > span {
+  color: #999999;
+  cursor: not-allowed;
+  background-color: #ffffff;
+}
+
+.label {
+  display: inline;
+  padding: .2em .6em .3em;
+  font-size: 75%;
+  font-weight: bold;
+  line-height: 1;
+  color: #ffffff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+}
+
+.label[href]:hover,
+.label[href]:focus {
+  color: #ffffff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.label:empty {
+  display: none;
+}
+
+.btn .label {
+  position: relative;
+  top: -1px;
+}
+
+.label-default {
+  background-color: #999999;
+}
+
+.label-default[href]:hover,
+.label-default[href]:focus {
+  background-color: #808080;
+}
+
+.label-primary {
+  background-color: #428bca;
+}
+
+.label-primary[href]:hover,
+.label-primary[href]:focus {
+  background-color: #3071a9;
+}
+
+.label-success {
+  background-color: #5cb85c;
+}
+
+.label-success[href]:hover,
+.label-success[href]:focus {
+  background-color: #449d44;
+}
+
+.label-info {
+  background-color: #5bc0de;
+}
+
+.label-info[href]:hover,
+.label-info[href]:focus {
+  background-color: #31b0d5;
+}
+
+.label-warning {
+  background-color: #f0ad4e;
+}
+
+.label-warning[href]:hover,
+.label-warning[href]:focus {
+  background-color: #ec971f;
+}
+
+.label-danger {
+  background-color: #d9534f;
+}
+
+.label-danger[href]:hover,
+.label-danger[href]:focus {
+  background-color: #c9302c;
+}
+
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  line-height: 1;
+  color: #ffffff;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  background-color: #999999;
+  border-radius: 10px;
+}
+
+.badge:empty {
+  display: none;
+}
+
+.btn .badge {
+  position: relative;
+  top: -1px;
+}
+
+a.badge:hover,
+a.badge:focus {
+  color: #ffffff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+a.list-group-item.active > .badge,
+.nav-pills > .active > a > .badge {
+  color: #428bca;
+  background-color: #ffffff;
+}
+
+.nav-pills > li > a > .badge {
+  margin-left: 3px;
+}
+
+.jumbotron {
+  padding: 30px;
+  margin-bottom: 30px;
+  font-size: 21px;
+  font-weight: 200;
+  line-height: 2.1428571435;
+  color: inherit;
+  background-color: #eeeeee;
+}
+
+.jumbotron h1,
+.jumbotron .h1 {
+  line-height: 1;
+  color: inherit;
+}
+
+.jumbotron p {
+  line-height: 1.4;
+}
+
+.container .jumbotron {
+  border-radius: 6px;
+}
+
+.jumbotron .container {
+  max-width: 100%;
+}
+
+@media screen and (min-width: 768px) {
+  .jumbotron {
+    padding-top: 48px;
+    padding-bottom: 48px;
+  }
+  .container .jumbotron {
+    padding-right: 60px;
+    padding-left: 60px;
+  }
+  .jumbotron h1,
+  .jumbotron .h1 {
+    font-size: 63px;
+  }
+}
+
+.thumbnail {
+  display: block;
+  padding: 4px;
+  margin-bottom: 20px;
+  line-height: 1.428571429;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-in-out;
+          transition: all 0.2s ease-in-out;
+}
+
+.thumbnail > img,
+.thumbnail a > img {
+  display: block;
+  height: auto;
+  max-width: 100%;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+a.thumbnail:hover,
+a.thumbnail:focus,
+a.thumbnail.active {
+  border-color: #428bca;
+}
+
+.thumbnail .caption {
+  padding: 9px;
+  color: #333333;
+}
+
+.alert {
+  padding: 15px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+  border-radius: 4px;
+}
+
+.alert h4 {
+  margin-top: 0;
+  color: inherit;
+}
+
+.alert .alert-link {
+  font-weight: bold;
+}
+
+.alert > p,
+.alert > ul {
+  margin-bottom: 0;
+}
+
+.alert > p + p {
+  margin-top: 5px;
+}
+
+.alert-dismissable {
+  padding-right: 35px;
+}
+
+.alert-dismissable .close {
+  position: relative;
+  top: -2px;
+  right: -21px;
+  color: inherit;
+}
+
+.alert-success {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+
+.alert-success hr {
+  border-top-color: #c9e2b3;
+}
+
+.alert-success .alert-link {
+  color: #2b542c;
+}
+
+.alert-info {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+
+.alert-info hr {
+  border-top-color: #a6e1ec;
+}
+
+.alert-info .alert-link {
+  color: #245269;
+}
+
+.alert-warning {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+
+.alert-warning hr {
+  border-top-color: #f7e1b5;
+}
+
+.alert-warning .alert-link {
+  color: #66512c;
+}
+
+.alert-danger {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+
+.alert-danger hr {
+  border-top-color: #e4b9c0;
+}
+
+.alert-danger .alert-link {
+  color: #843534;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
+}
+
+.progress {
+  height: 20px;
+  margin-bottom: 20px;
+  overflow: hidden;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+          box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.progress-bar {
+  float: left;
+  width: 0;
+  height: 100%;
+  font-size: 12px;
+  line-height: 20px;
+  color: #ffffff;
+  text-align: center;
+  background-color: #428bca;
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+          box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -webkit-transition: width 0.6s ease;
+          transition: width 0.6s ease;
+}
+
+.progress-striped .progress-bar {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-size: 40px 40px;
+}
+
+.progress.active .progress-bar {
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+          animation: progress-bar-stripes 2s linear infinite;
+}
+
+.progress-bar-success {
+  background-color: #5cb85c;
+}
+
+.progress-striped .progress-bar-success {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-info {
+  background-color: #5bc0de;
+}
+
+.progress-striped .progress-bar-info {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-warning {
+  background-color: #f0ad4e;
+}
+
+.progress-striped .progress-bar-warning {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.progress-bar-danger {
+  background-color: #d9534f;
+}
+
+.progress-striped .progress-bar-danger {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+}
+
+.media,
+.media-body {
+  overflow: hidden;
+  zoom: 1;
+}
+
+.media,
+.media .media {
+  margin-top: 15px;
+}
+
+.media:first-child {
+  margin-top: 0;
+}
+
+.media-object {
+  display: block;
+}
+
+.media-heading {
+  margin: 0 0 5px;
+}
+
+.media > .pull-left {
+  margin-right: 10px;
+}
+
+.media > .pull-right {
+  margin-left: 10px;
+}
+
+.media-list {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-group {
+  padding-left: 0;
+  margin-bottom: 20px;
+}
+
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+  margin-bottom: -1px;
+  background-color: #ffffff;
+  border: 1px solid #dddddd;
+}
+
+.list-group-item:first-child {
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+}
+
+.list-group-item:last-child {
+  margin-bottom: 0;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+
+.list-group-item > .badge {
+  float: right;
+}
+
+.list-group-item > .badge + .badge {
+  margin-right: 5px;
+}
+
+a.list-group-item {
+  color: #555555;
+}
+
+a.list-group-item .list-group-item-heading {
+  color: #333333;
+}
+
+a.list-group-item:hover,
+a.list-group-item:focus {
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+
+a.list-group-item.active,
+a.list-group-item.active:hover,
+a.list-group-item.active:focus {
+  z-index: 2;
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+a.list-group-item.active .list-group-item-heading,
+a.list-group-item.active:hover .list-group-item-heading,
+a.list-group-item.active:focus .list-group-item-heading {
+  color: inherit;
+}
+
+a.list-group-item.active .list-group-item-text,
+a.list-group-item.active:hover .list-group-item-text,
+a.list-group-item.active:focus .list-group-item-text {
+  color: #e1edf7;
+}
+
+.list-group-item-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+
+.list-group-item-text {
+  margin-bottom: 0;
+  line-height: 1.3;
+}
+
+.panel {
+  margin-bottom: 20px;
+  background-color: #ffffff;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+          box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.panel-body {
+  padding: 15px;
+}
+
+.panel-body:before,
+.panel-body:after {
+  display: table;
+  content: " ";
+}
+
+.panel-body:after {
+  clear: both;
+}
+
+.panel-body:before,
+.panel-body:after {
+  display: table;
+  content: " ";
+}
+
+.panel-body:after {
+  clear: both;
+}
+
+.panel > .list-group {
+  margin-bottom: 0;
+}
+
+.panel > .list-group .list-group-item {
+  border-width: 1px 0;
+}
+
+.panel > .list-group .list-group-item:first-child {
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+
+.panel > .list-group .list-group-item:last-child {
+  border-bottom: 0;
+}
+
+.panel-heading + .list-group .list-group-item:first-child {
+  border-top-width: 0;
+}
+
+.panel > .table,
+.panel > .table-responsive > .table {
+  margin-bottom: 0;
+}
+
+.panel > .panel-body + .table,
+.panel > .panel-body + .table-responsive {
+  border-top: 1px solid #dddddd;
+}
+
+.panel > .table > tbody:first-child th,
+.panel > .table > tbody:first-child td {
+  border-top: 0;
+}
+
+.panel > .table-bordered,
+.panel > .table-responsive > .table-bordered {
+  border: 0;
+}
+
+.panel > .table-bordered > thead > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
+.panel > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
+.panel > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
+.panel > .table-bordered > thead > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
+.panel > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
+.panel > .table-bordered > tfoot > tr > td:first-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+  border-left: 0;
+}
+
+.panel > .table-bordered > thead > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
+.panel > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
+.panel > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
+.panel > .table-bordered > thead > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
+.panel > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
+.panel > .table-bordered > tfoot > tr > td:last-child,
+.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+  border-right: 0;
+}
+
+.panel > .table-bordered > thead > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > thead > tr:last-child > th,
+.panel > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,
+.panel > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th,
+.panel > .table-bordered > thead > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > thead > tr:last-child > td,
+.panel > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,
+.panel > .table-bordered > tfoot > tr:last-child > td,
+.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+  border-bottom: 0;
+}
+
+.panel > .table-responsive {
+  margin-bottom: 0;
+  border: 0;
+}
+
+.panel-heading {
+  padding: 10px 15px;
+  border-bottom: 1px solid transparent;
+  border-top-right-radius: 3px;
+  border-top-left-radius: 3px;
+}
+
+.panel-heading > .dropdown .dropdown-toggle {
+  color: inherit;
+}
+
+.panel-title {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 16px;
+  color: inherit;
+}
+
+.panel-title > a {
+  color: inherit;
+}
+
+.panel-footer {
+  padding: 10px 15px;
+  background-color: #f5f5f5;
+  border-top: 1px solid #dddddd;
+  border-bottom-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+
+.panel-group .panel {
+  margin-bottom: 0;
+  overflow: hidden;
+  border-radius: 4px;
+}
+
+.panel-group .panel + .panel {
+  margin-top: 5px;
+}
+
+.panel-group .panel-heading {
+  border-bottom: 0;
+}
+
+.panel-group .panel-heading + .panel-collapse .panel-body {
+  border-top: 1px solid #dddddd;
+}
+
+.panel-group .panel-footer {
+  border-top: 0;
+}
+
+.panel-group .panel-footer + .panel-collapse .panel-body {
+  border-bottom: 1px solid #dddddd;
+}
+
+.panel-default {
+  border-color: #dddddd;
+}
+
+.panel-default > .panel-heading {
+  color: #333333;
+  background-color: #f5f5f5;
+  border-color: #dddddd;
+}
+
+.panel-default > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #dddddd;
+}
+
+.panel-default > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #dddddd;
+}
+
+.panel-primary {
+  border-color: #428bca;
+}
+
+.panel-primary > .panel-heading {
+  color: #ffffff;
+  background-color: #428bca;
+  border-color: #428bca;
+}
+
+.panel-primary > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #428bca;
+}
+
+.panel-primary > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #428bca;
+}
+
+.panel-success {
+  border-color: #d6e9c6;
+}
+
+.panel-success > .panel-heading {
+  color: #3c763d;
+  background-color: #dff0d8;
+  border-color: #d6e9c6;
+}
+
+.panel-success > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #d6e9c6;
+}
+
+.panel-success > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #d6e9c6;
+}
+
+.panel-warning {
+  border-color: #faebcc;
+}
+
+.panel-warning > .panel-heading {
+  color: #8a6d3b;
+  background-color: #fcf8e3;
+  border-color: #faebcc;
+}
+
+.panel-warning > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #faebcc;
+}
+
+.panel-warning > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #faebcc;
+}
+
+.panel-danger {
+  border-color: #ebccd1;
+}
+
+.panel-danger > .panel-heading {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+
+.panel-danger > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #ebccd1;
+}
+
+.panel-danger > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #ebccd1;
+}
+
+.panel-info {
+  border-color: #bce8f1;
+}
+
+.panel-info > .panel-heading {
+  color: #31708f;
+  background-color: #d9edf7;
+  border-color: #bce8f1;
+}
+
+.panel-info > .panel-heading + .panel-collapse .panel-body {
+  border-top-color: #bce8f1;
+}
+
+.panel-info > .panel-footer + .panel-collapse .panel-body {
+  border-bottom-color: #bce8f1;
+}
+
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border: 1px solid #e3e3e3;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.well blockquote {
+  border-color: #ddd;
+  border-color: rgba(0, 0, 0, 0.15);
+}
+
+.well-lg {
+  padding: 24px;
+  border-radius: 6px;
+}
+
+.well-sm {
+  padding: 9px;
+  border-radius: 3px;
+}
+
+.close {
+  float: right;
+  font-size: 21px;
+  font-weight: bold;
+  line-height: 1;
+  color: #000000;
+  text-shadow: 0 1px 0 #ffffff;
+  opacity: 0.2;
+  filter: alpha(opacity=20);
+}
+
+.close:hover,
+.close:focus {
+  color: #000000;
+  text-decoration: none;
+  cursor: pointer;
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+button.close {
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+}
+
+.modal-open {
+  overflow: hidden;
+}
+
+.modal {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1040;
+  display: none;
+  overflow: auto;
+  overflow-y: scroll;
+}
+
+.modal.fade .modal-dialog {
+  -webkit-transform: translate(0, -25%);
+      -ms-transform: translate(0, -25%);
+          transform: translate(0, -25%);
+  -webkit-transition: -webkit-transform 0.3s ease-out;
+     -moz-transition: -moz-transform 0.3s ease-out;
+       -o-transition: -o-transform 0.3s ease-out;
+          transition: transform 0.3s ease-out;
+}
+
+.modal.in .modal-dialog {
+  -webkit-transform: translate(0, 0);
+      -ms-transform: translate(0, 0);
+          transform: translate(0, 0);
+}
+
+.modal-dialog {
+  position: relative;
+  z-index: 1050;
+  width: auto;
+  margin: 10px;
+}
+
+.modal-content {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #999999;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  outline: none;
+  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+          box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+  background-clip: padding-box;
+}
+
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1030;
+  background-color: #000000;
+}
+
+.modal-backdrop.fade {
+  opacity: 0;
+  filter: alpha(opacity=0);
+}
+
+.modal-backdrop.in {
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+.modal-header {
+  min-height: 16.428571429px;
+  padding: 15px;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.modal-header .close {
+  margin-top: -2px;
+}
+
+.modal-title {
+  margin: 0;
+  line-height: 1.428571429;
+}
+
+.modal-body {
+  position: relative;
+  padding: 20px;
+}
+
+.modal-footer {
+  padding: 19px 20px 20px;
+  margin-top: 15px;
+  text-align: right;
+  border-top: 1px solid #e5e5e5;
+}
+
+.modal-footer:before,
+.modal-footer:after {
+  display: table;
+  content: " ";
+}
+
+.modal-footer:after {
+  clear: both;
+}
+
+.modal-footer:before,
+.modal-footer:after {
+  display: table;
+  content: " ";
+}
+
+.modal-footer:after {
+  clear: both;
+}
+
+.modal-footer .btn + .btn {
+  margin-bottom: 0;
+  margin-left: 5px;
+}
+
+.modal-footer .btn-group .btn + .btn {
+  margin-left: -1px;
+}
+
+.modal-footer .btn-block + .btn-block {
+  margin-left: 0;
+}
+
+@media screen and (min-width: 768px) {
+  .modal-dialog {
+    width: 600px;
+    margin: 30px auto;
+  }
+  .modal-content {
+    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
+  }
+}
+
+.tooltip {
+  position: absolute;
+  z-index: 1030;
+  display: block;
+  font-size: 12px;
+  line-height: 1.4;
+  opacity: 0;
+  filter: alpha(opacity=0);
+  visibility: visible;
+}
+
+.tooltip.in {
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+
+.tooltip.top {
+  padding: 5px 0;
+  margin-top: -3px;
+}
+
+.tooltip.right {
+  padding: 0 5px;
+  margin-left: 3px;
+}
+
+.tooltip.bottom {
+  padding: 5px 0;
+  margin-top: 3px;
+}
+
+.tooltip.left {
+  padding: 0 5px;
+  margin-left: -3px;
+}
+
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #ffffff;
+  text-align: center;
+  text-decoration: none;
+  background-color: #000000;
+  border-radius: 4px;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.top-left .tooltip-arrow {
+  bottom: 0;
+  left: 5px;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.top-right .tooltip-arrow {
+  right: 5px;
+  bottom: 0;
+  border-top-color: #000000;
+  border-width: 5px 5px 0;
+}
+
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-right-color: #000000;
+  border-width: 5px 5px 5px 0;
+}
+
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-left-color: #000000;
+  border-width: 5px 0 5px 5px;
+}
+
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.tooltip.bottom-left .tooltip-arrow {
+  top: 0;
+  left: 5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.tooltip.bottom-right .tooltip-arrow {
+  top: 0;
+  right: 5px;
+  border-bottom-color: #000000;
+  border-width: 0 5px 5px;
+}
+
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1010;
+  display: none;
+  max-width: 276px;
+  padding: 1px;
+  text-align: left;
+  white-space: normal;
+  background-color: #ffffff;
+  border: 1px solid #cccccc;
+  border: 1px solid rgba(0, 0, 0, 0.2);
+  border-radius: 6px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+          box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  background-clip: padding-box;
+}
+
+.popover.top {
+  margin-top: -10px;
+}
+
+.popover.right {
+  margin-left: 10px;
+}
+
+.popover.bottom {
+  margin-top: 10px;
+}
+
+.popover.left {
+  margin-left: -10px;
+}
+
+.popover-title {
+  padding: 8px 14px;
+  margin: 0;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 18px;
+  background-color: #f7f7f7;
+  border-bottom: 1px solid #ebebeb;
+  border-radius: 5px 5px 0 0;
+}
+
+.popover-content {
+  padding: 9px 14px;
+}
+
+.popover .arrow,
+.popover .arrow:after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.popover .arrow {
+  border-width: 11px;
+}
+
+.popover .arrow:after {
+  border-width: 10px;
+  content: "";
+}
+
+.popover.top .arrow {
+  bottom: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-top-color: #999999;
+  border-top-color: rgba(0, 0, 0, 0.25);
+  border-bottom-width: 0;
+}
+
+.popover.top .arrow:after {
+  bottom: 1px;
+  margin-left: -10px;
+  border-top-color: #ffffff;
+  border-bottom-width: 0;
+  content: " ";
+}
+
+.popover.right .arrow {
+  top: 50%;
+  left: -11px;
+  margin-top: -11px;
+  border-right-color: #999999;
+  border-right-color: rgba(0, 0, 0, 0.25);
+  border-left-width: 0;
+}
+
+.popover.right .arrow:after {
+  bottom: -10px;
+  left: 1px;
+  border-right-color: #ffffff;
+  border-left-width: 0;
+  content: " ";
+}
+
+.popover.bottom .arrow {
+  top: -11px;
+  left: 50%;
+  margin-left: -11px;
+  border-bottom-color: #999999;
+  border-bottom-color: rgba(0, 0, 0, 0.25);
+  border-top-width: 0;
+}
+
+.popover.bottom .arrow:after {
+  top: 1px;
+  margin-left: -10px;
+  border-bottom-color: #ffffff;
+  border-top-width: 0;
+  content: " ";
+}
+
+.popover.left .arrow {
+  top: 50%;
+  right: -11px;
+  margin-top: -11px;
+  border-left-color: #999999;
+  border-left-color: rgba(0, 0, 0, 0.25);
+  border-right-width: 0;
+}
+
+.popover.left .arrow:after {
+  right: 1px;
+  bottom: -10px;
+  border-left-color: #ffffff;
+  border-right-width: 0;
+  content: " ";
+}
+
+.carousel {
+  position: relative;
+}
+
+.carousel-inner {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+}
+
+.carousel-inner > .item {
+  position: relative;
+  display: none;
+  -webkit-transition: 0.6s ease-in-out left;
+          transition: 0.6s ease-in-out left;
+}
+
+.carousel-inner > .item > img,
+.carousel-inner > .item > a > img {
+  display: block;
+  height: auto;
+  max-width: 100%;
+  line-height: 1;
+}
+
+.carousel-inner > .active,
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  display: block;
+}
+
+.carousel-inner > .active {
+  left: 0;
+}
+
+.carousel-inner > .next,
+.carousel-inner > .prev {
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+
+.carousel-inner > .next {
+  left: 100%;
+}
+
+.carousel-inner > .prev {
+  left: -100%;
+}
+
+.carousel-inner > .next.left,
+.carousel-inner > .prev.right {
+  left: 0;
+}
+
+.carousel-inner > .active.left {
+  left: -100%;
+}
+
+.carousel-inner > .active.right {
+  left: 100%;
+}
+
+.carousel-control {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  width: 15%;
+  font-size: 20px;
+  color: #ffffff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+}
+
+.carousel-control.left {
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0), color-stop(rgba(0, 0, 0, 0.0001) 100%));
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
+}
+
+.carousel-control.right {
+  right: 0;
+  left: auto;
+  background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0), color-stop(rgba(0, 0, 0, 0.5) 100%));
+  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
+}
+
+.carousel-control:hover,
+.carousel-control:focus {
+  color: #ffffff;
+  text-decoration: none;
+  outline: none;
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+
+.carousel-control .icon-prev,
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-left,
+.carousel-control .glyphicon-chevron-right {
+  position: absolute;
+  top: 50%;
+  z-index: 5;
+  display: inline-block;
+}
+
+.carousel-control .icon-prev,
+.carousel-control .glyphicon-chevron-left {
+  left: 50%;
+}
+
+.carousel-control .icon-next,
+.carousel-control .glyphicon-chevron-right {
+  right: 50%;
+}
+
+.carousel-control .icon-prev,
+.carousel-control .icon-next {
+  width: 20px;
+  height: 20px;
+  margin-top: -10px;
+  margin-left: -10px;
+  font-family: serif;
+}
+
+.carousel-control .icon-prev:before {
+  content: '\2039';
+}
+
+.carousel-control .icon-next:before {
+  content: '\203a';
+}
+
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  padding-left: 0;
+  margin-left: -30%;
+  text-align: center;
+  list-style: none;
+}
+
+.carousel-indicators li {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin: 1px;
+  text-indent: -999px;
+  cursor: pointer;
+  background-color: #000 \9;
+  background-color: rgba(0, 0, 0, 0);
+  border: 1px solid #ffffff;
+  border-radius: 10px;
+}
+
+.carousel-indicators .active {
+  width: 12px;
+  height: 12px;
+  margin: 0;
+  background-color: #ffffff;
+}
+
+.carousel-caption {
+  position: absolute;
+  right: 15%;
+  bottom: 20px;
+  left: 15%;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #ffffff;
+  text-align: center;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
+}
+
+.carousel-caption .btn {
+  text-shadow: none;
+}
+
+@media screen and (min-width: 768px) {
+  .carousel-control .glyphicons-chevron-left,
+  .carousel-control .glyphicons-chevron-right,
+  .carousel-control .icon-prev,
+  .carousel-control .icon-next {
+    width: 30px;
+    height: 30px;
+    margin-top: -15px;
+    margin-left: -15px;
+    font-size: 30px;
+  }
+  .carousel-caption {
+    right: 20%;
+    left: 20%;
+    padding-bottom: 30px;
+  }
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}
+
+.clearfix:before,
+.clearfix:after {
+  display: table;
+  content: " ";
+}
+
+.clearfix:after {
+  clear: both;
+}
+
+.center-block {
+  display: block;
+  margin-right: auto;
+  margin-left: auto;
+}
+
+.pull-right {
+  float: right !important;
+}
+
+.pull-left {
+  float: left !important;
+}
+
+.hide {
+  display: none !important;
+}
+
+.show {
+  display: block !important;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.text-hide {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+.hidden {
+  display: none !important;
+  visibility: hidden !important;
+}
+
+.affix {
+  position: fixed;
+}
+
+@-ms-viewport {
+  width: device-width;
+}
+
+.visible-xs,
+tr.visible-xs,
+th.visible-xs,
+td.visible-xs {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-xs {
+    display: block !important;
+  }
+  table.visible-xs {
+    display: table;
+  }
+  tr.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-xs,
+  td.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-xs.visible-sm {
+    display: block !important;
+  }
+  table.visible-xs.visible-sm {
+    display: table;
+  }
+  tr.visible-xs.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-sm,
+  td.visible-xs.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-xs.visible-md {
+    display: block !important;
+  }
+  table.visible-xs.visible-md {
+    display: table;
+  }
+  tr.visible-xs.visible-md {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-md,
+  td.visible-xs.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-xs.visible-lg {
+    display: block !important;
+  }
+  table.visible-xs.visible-lg {
+    display: table;
+  }
+  tr.visible-xs.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-xs.visible-lg,
+  td.visible-xs.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-sm,
+tr.visible-sm,
+th.visible-sm,
+td.visible-sm {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-sm.visible-xs {
+    display: block !important;
+  }
+  table.visible-sm.visible-xs {
+    display: table;
+  }
+  tr.visible-sm.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-xs,
+  td.visible-sm.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-sm {
+    display: block !important;
+  }
+  table.visible-sm {
+    display: table;
+  }
+  tr.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-sm,
+  td.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-sm.visible-md {
+    display: block !important;
+  }
+  table.visible-sm.visible-md {
+    display: table;
+  }
+  tr.visible-sm.visible-md {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-md,
+  td.visible-sm.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-sm.visible-lg {
+    display: block !important;
+  }
+  table.visible-sm.visible-lg {
+    display: table;
+  }
+  tr.visible-sm.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-sm.visible-lg,
+  td.visible-sm.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-md,
+tr.visible-md,
+th.visible-md,
+td.visible-md {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-md.visible-xs {
+    display: block !important;
+  }
+  table.visible-md.visible-xs {
+    display: table;
+  }
+  tr.visible-md.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-md.visible-xs,
+  td.visible-md.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-md.visible-sm {
+    display: block !important;
+  }
+  table.visible-md.visible-sm {
+    display: table;
+  }
+  tr.visible-md.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-md.visible-sm,
+  td.visible-md.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-md {
+    display: block !important;
+  }
+  table.visible-md {
+    display: table;
+  }
+  tr.visible-md {
+    display: table-row !important;
+  }
+  th.visible-md,
+  td.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-md.visible-lg {
+    display: block !important;
+  }
+  table.visible-md.visible-lg {
+    display: table;
+  }
+  tr.visible-md.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-md.visible-lg,
+  td.visible-md.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.visible-lg,
+tr.visible-lg,
+th.visible-lg,
+td.visible-lg {
+  display: none !important;
+}
+
+@media (max-width: 767px) {
+  .visible-lg.visible-xs {
+    display: block !important;
+  }
+  table.visible-lg.visible-xs {
+    display: table;
+  }
+  tr.visible-lg.visible-xs {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-xs,
+  td.visible-lg.visible-xs {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .visible-lg.visible-sm {
+    display: block !important;
+  }
+  table.visible-lg.visible-sm {
+    display: table;
+  }
+  tr.visible-lg.visible-sm {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-sm,
+  td.visible-lg.visible-sm {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .visible-lg.visible-md {
+    display: block !important;
+  }
+  table.visible-lg.visible-md {
+    display: table;
+  }
+  tr.visible-lg.visible-md {
+    display: table-row !important;
+  }
+  th.visible-lg.visible-md,
+  td.visible-lg.visible-md {
+    display: table-cell !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .visible-lg {
+    display: block !important;
+  }
+  table.visible-lg {
+    display: table;
+  }
+  tr.visible-lg {
+    display: table-row !important;
+  }
+  th.visible-lg,
+  td.visible-lg {
+    display: table-cell !important;
+  }
+}
+
+.hidden-xs {
+  display: block !important;
+}
+
+table.hidden-xs {
+  display: table;
+}
+
+tr.hidden-xs {
+  display: table-row !important;
+}
+
+th.hidden-xs,
+td.hidden-xs {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-xs,
+  tr.hidden-xs,
+  th.hidden-xs,
+  td.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-xs.hidden-sm,
+  tr.hidden-xs.hidden-sm,
+  th.hidden-xs.hidden-sm,
+  td.hidden-xs.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-xs.hidden-md,
+  tr.hidden-xs.hidden-md,
+  th.hidden-xs.hidden-md,
+  td.hidden-xs.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-xs.hidden-lg,
+  tr.hidden-xs.hidden-lg,
+  th.hidden-xs.hidden-lg,
+  td.hidden-xs.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-sm {
+  display: block !important;
+}
+
+table.hidden-sm {
+  display: table;
+}
+
+tr.hidden-sm {
+  display: table-row !important;
+}
+
+th.hidden-sm,
+td.hidden-sm {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-sm.hidden-xs,
+  tr.hidden-sm.hidden-xs,
+  th.hidden-sm.hidden-xs,
+  td.hidden-sm.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-sm,
+  tr.hidden-sm,
+  th.hidden-sm,
+  td.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-sm.hidden-md,
+  tr.hidden-sm.hidden-md,
+  th.hidden-sm.hidden-md,
+  td.hidden-sm.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-sm.hidden-lg,
+  tr.hidden-sm.hidden-lg,
+  th.hidden-sm.hidden-lg,
+  td.hidden-sm.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-md {
+  display: block !important;
+}
+
+table.hidden-md {
+  display: table;
+}
+
+tr.hidden-md {
+  display: table-row !important;
+}
+
+th.hidden-md,
+td.hidden-md {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-md.hidden-xs,
+  tr.hidden-md.hidden-xs,
+  th.hidden-md.hidden-xs,
+  td.hidden-md.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-md.hidden-sm,
+  tr.hidden-md.hidden-sm,
+  th.hidden-md.hidden-sm,
+  td.hidden-md.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-md,
+  tr.hidden-md,
+  th.hidden-md,
+  td.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-md.hidden-lg,
+  tr.hidden-md.hidden-lg,
+  th.hidden-md.hidden-lg,
+  td.hidden-md.hidden-lg {
+    display: none !important;
+  }
+}
+
+.hidden-lg {
+  display: block !important;
+}
+
+table.hidden-lg {
+  display: table;
+}
+
+tr.hidden-lg {
+  display: table-row !important;
+}
+
+th.hidden-lg,
+td.hidden-lg {
+  display: table-cell !important;
+}
+
+@media (max-width: 767px) {
+  .hidden-lg.hidden-xs,
+  tr.hidden-lg.hidden-xs,
+  th.hidden-lg.hidden-xs,
+  td.hidden-lg.hidden-xs {
+    display: none !important;
+  }
+}
+
+@media (min-width: 768px) and (max-width: 991px) {
+  .hidden-lg.hidden-sm,
+  tr.hidden-lg.hidden-sm,
+  th.hidden-lg.hidden-sm,
+  td.hidden-lg.hidden-sm {
+    display: none !important;
+  }
+}
+
+@media (min-width: 992px) and (max-width: 1199px) {
+  .hidden-lg.hidden-md,
+  tr.hidden-lg.hidden-md,
+  th.hidden-lg.hidden-md,
+  td.hidden-lg.hidden-md {
+    display: none !important;
+  }
+}
+
+@media (min-width: 1200px) {
+  .hidden-lg,
+  tr.hidden-lg,
+  th.hidden-lg,
+  td.hidden-lg {
+    display: none !important;
+  }
+}
+
+.visible-print,
+tr.visible-print,
+th.visible-print,
+td.visible-print {
+  display: none !important;
+}
+
+@media print {
+  .visible-print {
+    display: block !important;
+  }
+  table.visible-print {
+    display: table;
+  }
+  tr.visible-print {
+    display: table-row !important;
+  }
+  th.visible-print,
+  td.visible-print {
+    display: table-cell !important;
+  }
+  .hidden-print,
+  tr.hidden-print,
+  th.hidden-print,
+  td.hidden-print {
+    display: none !important;
+  }
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.min.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.min.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/css/bootstrap.min.css	(revision 16045)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2014 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}}
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/fonts/glyphicons-halflings-regular.svg	(revision 16045)
@@ -0,0 +1,229 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
+<font-face units-per-em="1200" ascent="960" descent="-240" />
+<missing-glyph horiz-adv-x="500" />
+<glyph />
+<glyph />
+<glyph unicode="&#xd;" />
+<glyph unicode=" " />
+<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
+<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
+<glyph unicode="&#xa0;" />
+<glyph unicode="&#x2000;" horiz-adv-x="652" />
+<glyph unicode="&#x2001;" horiz-adv-x="1304" />
+<glyph unicode="&#x2002;" horiz-adv-x="652" />
+<glyph unicode="&#x2003;" horiz-adv-x="1304" />
+<glyph unicode="&#x2004;" horiz-adv-x="434" />
+<glyph unicode="&#x2005;" horiz-adv-x="326" />
+<glyph unicode="&#x2006;" horiz-adv-x="217" />
+<glyph unicode="&#x2007;" horiz-adv-x="217" />
+<glyph unicode="&#x2008;" horiz-adv-x="163" />
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
+<glyph unicode="&#x205f;" horiz-adv-x="326" />
+<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
+<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
+<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
+<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
+<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
+<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
+<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
+<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
+<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
+<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
+<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
+<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
+<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
+<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
+<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
+<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
+<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
+<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
+<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
+<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
+<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
+<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
+<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
+<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
+<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
+<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
+<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
+<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
+<glyph unicode="&#xe028;" d="M0 25v475l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
+<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
+<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
+<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
+<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
+<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
+<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
+<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
+<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
+<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
+<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
+<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
+<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
+<glyph unicode="&#xe041;" d="M1 700v475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
+<glyph unicode="&#xe042;" d="M2 700v475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
+<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
+<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
+<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
+<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
+<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
+<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v70h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
+<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
+<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
+<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
+<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
+<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
+<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
+<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
+<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
+<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
+<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
+<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
+<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l566 567l-136 137l-430 -431l-147 147z" />
+<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
+<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
+<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
+<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
+<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
+<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
+<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
+<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
+<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
+<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
+<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h600v200h-600v-200z" />
+<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141z" />
+<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
+<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM363 700h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26 q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-105 0 -172 -56t-67 -183zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
+<glyph unicode="&#xe087;" d="M0 500v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200 v-206q149 48 201 206h-201v200h200q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210z" />
+<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
+<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
+<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
+<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
+<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
+<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
+<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
+<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
+<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
+<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
+<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100z M100 0h400v400h-400v-400zM200 900q-3 0 14 48t35 96l18 47l214 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
+<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
+<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
+<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
+<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
+<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
+<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64 q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
+<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
+<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
+<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
+<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
+<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
+<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
+<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
+<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
+<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
+<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
+<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
+<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
+<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
+<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM99 500v250v5q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351z M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37 t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
+<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
+<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 212l100 213h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
+<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
+<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
+<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM999 201v600h200v-600h-200z" />
+<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
+<glyph unicode="&#xe130;" d="M1 585q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM76 565l237 339h503l89 -100v-294l-340 -130 q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
+<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 500h300l-2 -194l402 294l-402 298v-197h-298v-201z" />
+<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l400 -294v194h302v201h-300v197z" />
+<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
+<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
+<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -34 5.5 -93t7.5 -87q0 -9 17 -44t16 -60q12 0 23 -5.5 t23 -15t20 -13.5q20 -10 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55.5t-20 -57.5q12 -21 22.5 -34.5t28 -27t36.5 -17.5q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q101 -2 221 111q31 30 47 48t34 49t21 62q-14 9 -37.5 9.5t-35.5 7.5q-14 7 -49 15t-52 19 q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q8 16 22 22q6 -1 26 -1.5t33.5 -4.5t19.5 -13q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5 t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23 q-19 -3 -37 0q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -46 0t-45 -3q-20 -6 -51.5 -25.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79zM518 915q3 12 16 30.5t16 25.5q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -18 8 -42.5t16.5 -44 t9.5 -23.5q-6 1 -39 5t-53.5 10t-36.5 16z" />
+<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
+<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
+<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
+<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
+<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
+<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM513 609q0 32 21 56.5t52 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-16 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5q-37 0 -62.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
+<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36 q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60l517 511 q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
+<glyph unicode="&#xe143;" d="M79 784q0 131 99 229.5t230 98.5q144 0 242 -129q103 129 245 129q130 0 227 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100l-84.5 84.5t-68 74t-60 78t-33.5 70.5t-15 78z M250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-106 48.5q-73 0 -131 -83l-118 -171l-114 174q-51 80 -124 80q-59 0 -108.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe144;" d="M57 353q0 -94 66 -160l141 -141q66 -66 159 -66q95 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141l19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
+<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
+<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
+<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5v-307l64 -14 q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5zM700 237 q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
+<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5 t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10 t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221z" />
+<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
+<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
+<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
+<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
+<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
+<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
+<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
+<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
+<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
+<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
+<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
+<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
+<glyph unicode="&#xe162;" d="M216 519q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40z" />
+<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
+<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
+<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
+<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 401h700v699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l248 -237v700h-699zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
+<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
+<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
+<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
+<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359z" />
+<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
+<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
+<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118q17 17 20 41.5 t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
+<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
+<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
+<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
+<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
+<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
+<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
+<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
+<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
+<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
+<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300 h200l-300 -300z" />
+<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
+<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
+<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
+</font>
+</defs></svg> 
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.js	(revision 16045)
@@ -0,0 +1,2006 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2013 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+if (typeof jQuery === "undefined") { throw new Error("Bootstrap requires jQuery") }
+
+/* ========================================================================
+ * Bootstrap: transition.js v3.0.3
+ * http://getbootstrap.com/javascript/#transitions
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
+  // ============================================================
+
+  function transitionEnd() {
+    var el = document.createElement('bootstrap')
+
+    var transEndEventNames = {
+      'WebkitTransition' : 'webkitTransitionEnd'
+    , 'MozTransition'    : 'transitionend'
+    , 'OTransition'      : 'oTransitionEnd otransitionend'
+    , 'transition'       : 'transitionend'
+    }
+
+    for (var name in transEndEventNames) {
+      if (el.style[name] !== undefined) {
+        return { end: transEndEventNames[name] }
+      }
+    }
+  }
+
+  // http://blog.alexmaccaw.com/css-transitions
+  $.fn.emulateTransitionEnd = function (duration) {
+    var called = false, $el = this
+    $(this).one($.support.transition.end, function () { called = true })
+    var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
+    setTimeout(callback, duration)
+    return this
+  }
+
+  $(function () {
+    $.support.transition = transitionEnd()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: alert.js v3.0.3
+ * http://getbootstrap.com/javascript/#alerts
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // ALERT CLASS DEFINITION
+  // ======================
+
+  var dismiss = '[data-dismiss="alert"]'
+  var Alert   = function (el) {
+    $(el).on('click', dismiss, this.close)
+  }
+
+  Alert.prototype.close = function (e) {
+    var $this    = $(this)
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
+    }
+
+    var $parent = $(selector)
+
+    if (e) e.preventDefault()
+
+    if (!$parent.length) {
+      $parent = $this.hasClass('alert') ? $this : $this.parent()
+    }
+
+    $parent.trigger(e = $.Event('close.bs.alert'))
+
+    if (e.isDefaultPrevented()) return
+
+    $parent.removeClass('in')
+
+    function removeElement() {
+      $parent.trigger('closed.bs.alert').remove()
+    }
+
+    $.support.transition && $parent.hasClass('fade') ?
+      $parent
+        .one($.support.transition.end, removeElement)
+        .emulateTransitionEnd(150) :
+      removeElement()
+  }
+
+
+  // ALERT PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.alert
+
+  $.fn.alert = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.alert')
+
+      if (!data) $this.data('bs.alert', (data = new Alert(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.alert.Constructor = Alert
+
+
+  // ALERT NO CONFLICT
+  // =================
+
+  $.fn.alert.noConflict = function () {
+    $.fn.alert = old
+    return this
+  }
+
+
+  // ALERT DATA-API
+  // ==============
+
+  $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: button.js v3.0.3
+ * http://getbootstrap.com/javascript/#buttons
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // BUTTON PUBLIC CLASS DEFINITION
+  // ==============================
+
+  var Button = function (element, options) {
+    this.$element = $(element)
+    this.options  = $.extend({}, Button.DEFAULTS, options)
+  }
+
+  Button.DEFAULTS = {
+    loadingText: 'loading...'
+  }
+
+  Button.prototype.setState = function (state) {
+    var d    = 'disabled'
+    var $el  = this.$element
+    var val  = $el.is('input') ? 'val' : 'html'
+    var data = $el.data()
+
+    state = state + 'Text'
+
+    if (!data.resetText) $el.data('resetText', $el[val]())
+
+    $el[val](data[state] || this.options[state])
+
+    // push to event loop to allow forms to submit
+    setTimeout(function () {
+      state == 'loadingText' ?
+        $el.addClass(d).attr(d, d) :
+        $el.removeClass(d).removeAttr(d);
+    }, 0)
+  }
+
+  Button.prototype.toggle = function () {
+    var $parent = this.$element.closest('[data-toggle="buttons"]')
+    var changed = true
+
+    if ($parent.length) {
+      var $input = this.$element.find('input')
+      if ($input.prop('type') === 'radio') {
+        // see if clicking on current one
+        if ($input.prop('checked') && this.$element.hasClass('active'))
+          changed = false
+        else
+          $parent.find('.active').removeClass('active')
+      }
+      if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
+    }
+
+    if (changed) this.$element.toggleClass('active')
+  }
+
+
+  // BUTTON PLUGIN DEFINITION
+  // ========================
+
+  var old = $.fn.button
+
+  $.fn.button = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.button')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.button', (data = new Button(this, options)))
+
+      if (option == 'toggle') data.toggle()
+      else if (option) data.setState(option)
+    })
+  }
+
+  $.fn.button.Constructor = Button
+
+
+  // BUTTON NO CONFLICT
+  // ==================
+
+  $.fn.button.noConflict = function () {
+    $.fn.button = old
+    return this
+  }
+
+
+  // BUTTON DATA-API
+  // ===============
+
+  $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
+    var $btn = $(e.target)
+    if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
+    $btn.button('toggle')
+    e.preventDefault()
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: carousel.js v3.0.3
+ * http://getbootstrap.com/javascript/#carousel
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // CAROUSEL CLASS DEFINITION
+  // =========================
+
+  var Carousel = function (element, options) {
+    this.$element    = $(element)
+    this.$indicators = this.$element.find('.carousel-indicators')
+    this.options     = options
+    this.paused      =
+    this.sliding     =
+    this.interval    =
+    this.$active     =
+    this.$items      = null
+
+    this.options.pause == 'hover' && this.$element
+      .on('mouseenter', $.proxy(this.pause, this))
+      .on('mouseleave', $.proxy(this.cycle, this))
+  }
+
+  Carousel.DEFAULTS = {
+    interval: 5000
+  , pause: 'hover'
+  , wrap: true
+  }
+
+  Carousel.prototype.cycle =  function (e) {
+    e || (this.paused = false)
+
+    this.interval && clearInterval(this.interval)
+
+    this.options.interval
+      && !this.paused
+      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
+
+    return this
+  }
+
+  Carousel.prototype.getActiveIndex = function () {
+    this.$active = this.$element.find('.item.active')
+    this.$items  = this.$active.parent().children()
+
+    return this.$items.index(this.$active)
+  }
+
+  Carousel.prototype.to = function (pos) {
+    var that        = this
+    var activeIndex = this.getActiveIndex()
+
+    if (pos > (this.$items.length - 1) || pos < 0) return
+
+    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) })
+    if (activeIndex == pos) return this.pause().cycle()
+
+    return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
+  }
+
+  Carousel.prototype.pause = function (e) {
+    e || (this.paused = true)
+
+    if (this.$element.find('.next, .prev').length && $.support.transition.end) {
+      this.$element.trigger($.support.transition.end)
+      this.cycle(true)
+    }
+
+    this.interval = clearInterval(this.interval)
+
+    return this
+  }
+
+  Carousel.prototype.next = function () {
+    if (this.sliding) return
+    return this.slide('next')
+  }
+
+  Carousel.prototype.prev = function () {
+    if (this.sliding) return
+    return this.slide('prev')
+  }
+
+  Carousel.prototype.slide = function (type, next) {
+    var $active   = this.$element.find('.item.active')
+    var $next     = next || $active[type]()
+    var isCycling = this.interval
+    var direction = type == 'next' ? 'left' : 'right'
+    var fallback  = type == 'next' ? 'first' : 'last'
+    var that      = this
+
+    if (!$next.length) {
+      if (!this.options.wrap) return
+      $next = this.$element.find('.item')[fallback]()
+    }
+
+    this.sliding = true
+
+    isCycling && this.pause()
+
+    var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction })
+
+    if ($next.hasClass('active')) return
+
+    if (this.$indicators.length) {
+      this.$indicators.find('.active').removeClass('active')
+      this.$element.one('slid.bs.carousel', function () {
+        var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
+        $nextIndicator && $nextIndicator.addClass('active')
+      })
+    }
+
+    if ($.support.transition && this.$element.hasClass('slide')) {
+      this.$element.trigger(e)
+      if (e.isDefaultPrevented()) return
+      $next.addClass(type)
+      $next[0].offsetWidth // force reflow
+      $active.addClass(direction)
+      $next.addClass(direction)
+      $active
+        .one($.support.transition.end, function () {
+          $next.removeClass([type, direction].join(' ')).addClass('active')
+          $active.removeClass(['active', direction].join(' '))
+          that.sliding = false
+          setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0)
+        })
+        .emulateTransitionEnd(600)
+    } else {
+      this.$element.trigger(e)
+      if (e.isDefaultPrevented()) return
+      $active.removeClass('active')
+      $next.addClass('active')
+      this.sliding = false
+      this.$element.trigger('slid.bs.carousel')
+    }
+
+    isCycling && this.cycle()
+
+    return this
+  }
+
+
+  // CAROUSEL PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.carousel
+
+  $.fn.carousel = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.carousel')
+      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
+      var action  = typeof option == 'string' ? option : options.slide
+
+      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
+      if (typeof option == 'number') data.to(option)
+      else if (action) data[action]()
+      else if (options.interval) data.pause().cycle()
+    })
+  }
+
+  $.fn.carousel.Constructor = Carousel
+
+
+  // CAROUSEL NO CONFLICT
+  // ====================
+
+  $.fn.carousel.noConflict = function () {
+    $.fn.carousel = old
+    return this
+  }
+
+
+  // CAROUSEL DATA-API
+  // =================
+
+  $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
+    var $this   = $(this), href
+    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+    var options = $.extend({}, $target.data(), $this.data())
+    var slideIndex = $this.attr('data-slide-to')
+    if (slideIndex) options.interval = false
+
+    $target.carousel(options)
+
+    if (slideIndex = $this.attr('data-slide-to')) {
+      $target.data('bs.carousel').to(slideIndex)
+    }
+
+    e.preventDefault()
+  })
+
+  $(window).on('load', function () {
+    $('[data-ride="carousel"]').each(function () {
+      var $carousel = $(this)
+      $carousel.carousel($carousel.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: collapse.js v3.0.3
+ * http://getbootstrap.com/javascript/#collapse
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // COLLAPSE PUBLIC CLASS DEFINITION
+  // ================================
+
+  var Collapse = function (element, options) {
+    this.$element      = $(element)
+    this.options       = $.extend({}, Collapse.DEFAULTS, options)
+    this.transitioning = null
+
+    if (this.options.parent) this.$parent = $(this.options.parent)
+    if (this.options.toggle) this.toggle()
+  }
+
+  Collapse.DEFAULTS = {
+    toggle: true
+  }
+
+  Collapse.prototype.dimension = function () {
+    var hasWidth = this.$element.hasClass('width')
+    return hasWidth ? 'width' : 'height'
+  }
+
+  Collapse.prototype.show = function () {
+    if (this.transitioning || this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('show.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var actives = this.$parent && this.$parent.find('> .panel > .in')
+
+    if (actives && actives.length) {
+      var hasData = actives.data('bs.collapse')
+      if (hasData && hasData.transitioning) return
+      actives.collapse('hide')
+      hasData || actives.data('bs.collapse', null)
+    }
+
+    var dimension = this.dimension()
+
+    this.$element
+      .removeClass('collapse')
+      .addClass('collapsing')
+      [dimension](0)
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.$element
+        .removeClass('collapsing')
+        .addClass('in')
+        [dimension]('auto')
+      this.transitioning = 0
+      this.$element.trigger('shown.bs.collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    var scrollSize = $.camelCase(['scroll', dimension].join('-'))
+
+    this.$element
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+      [dimension](this.$element[0][scrollSize])
+  }
+
+  Collapse.prototype.hide = function () {
+    if (this.transitioning || !this.$element.hasClass('in')) return
+
+    var startEvent = $.Event('hide.bs.collapse')
+    this.$element.trigger(startEvent)
+    if (startEvent.isDefaultPrevented()) return
+
+    var dimension = this.dimension()
+
+    this.$element
+      [dimension](this.$element[dimension]())
+      [0].offsetHeight
+
+    this.$element
+      .addClass('collapsing')
+      .removeClass('collapse')
+      .removeClass('in')
+
+    this.transitioning = 1
+
+    var complete = function () {
+      this.transitioning = 0
+      this.$element
+        .trigger('hidden.bs.collapse')
+        .removeClass('collapsing')
+        .addClass('collapse')
+    }
+
+    if (!$.support.transition) return complete.call(this)
+
+    this.$element
+      [dimension](0)
+      .one($.support.transition.end, $.proxy(complete, this))
+      .emulateTransitionEnd(350)
+  }
+
+  Collapse.prototype.toggle = function () {
+    this[this.$element.hasClass('in') ? 'hide' : 'show']()
+  }
+
+
+  // COLLAPSE PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.collapse
+
+  $.fn.collapse = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.collapse')
+      var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.collapse.Constructor = Collapse
+
+
+  // COLLAPSE NO CONFLICT
+  // ====================
+
+  $.fn.collapse.noConflict = function () {
+    $.fn.collapse = old
+    return this
+  }
+
+
+  // COLLAPSE DATA-API
+  // =================
+
+  $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) {
+    var $this   = $(this), href
+    var target  = $this.attr('data-target')
+        || e.preventDefault()
+        || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
+    var $target = $(target)
+    var data    = $target.data('bs.collapse')
+    var option  = data ? 'toggle' : $this.data()
+    var parent  = $this.attr('data-parent')
+    var $parent = parent && $(parent)
+
+    if (!data || !data.transitioning) {
+      if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed')
+      $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
+    }
+
+    $target.collapse(option)
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: dropdown.js v3.0.3
+ * http://getbootstrap.com/javascript/#dropdowns
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // DROPDOWN CLASS DEFINITION
+  // =========================
+
+  var backdrop = '.dropdown-backdrop'
+  var toggle   = '[data-toggle=dropdown]'
+  var Dropdown = function (element) {
+    $(element).on('click.bs.dropdown', this.toggle)
+  }
+
+  Dropdown.prototype.toggle = function (e) {
+    var $this = $(this)
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    clearMenus()
+
+    if (!isActive) {
+      if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
+        // if mobile we use a backdrop because click events don't delegate
+        $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
+      }
+
+      $parent.trigger(e = $.Event('show.bs.dropdown'))
+
+      if (e.isDefaultPrevented()) return
+
+      $parent
+        .toggleClass('open')
+        .trigger('shown.bs.dropdown')
+
+      $this.focus()
+    }
+
+    return false
+  }
+
+  Dropdown.prototype.keydown = function (e) {
+    if (!/(38|40|27)/.test(e.keyCode)) return
+
+    var $this = $(this)
+
+    e.preventDefault()
+    e.stopPropagation()
+
+    if ($this.is('.disabled, :disabled')) return
+
+    var $parent  = getParent($this)
+    var isActive = $parent.hasClass('open')
+
+    if (!isActive || (isActive && e.keyCode == 27)) {
+      if (e.which == 27) $parent.find(toggle).focus()
+      return $this.click()
+    }
+
+    var $items = $('[role=menu] li:not(.divider):visible a', $parent)
+
+    if (!$items.length) return
+
+    var index = $items.index($items.filter(':focus'))
+
+    if (e.keyCode == 38 && index > 0)                 index--                        // up
+    if (e.keyCode == 40 && index < $items.length - 1) index++                        // down
+    if (!~index)                                      index=0
+
+    $items.eq(index).focus()
+  }
+
+  function clearMenus() {
+    $(backdrop).remove()
+    $(toggle).each(function (e) {
+      var $parent = getParent($(this))
+      if (!$parent.hasClass('open')) return
+      $parent.trigger(e = $.Event('hide.bs.dropdown'))
+      if (e.isDefaultPrevented()) return
+      $parent.removeClass('open').trigger('hidden.bs.dropdown')
+    })
+  }
+
+  function getParent($this) {
+    var selector = $this.attr('data-target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    var $parent = selector && $(selector)
+
+    return $parent && $parent.length ? $parent : $this.parent()
+  }
+
+
+  // DROPDOWN PLUGIN DEFINITION
+  // ==========================
+
+  var old = $.fn.dropdown
+
+  $.fn.dropdown = function (option) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.dropdown')
+
+      if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)))
+      if (typeof option == 'string') data[option].call($this)
+    })
+  }
+
+  $.fn.dropdown.Constructor = Dropdown
+
+
+  // DROPDOWN NO CONFLICT
+  // ====================
+
+  $.fn.dropdown.noConflict = function () {
+    $.fn.dropdown = old
+    return this
+  }
+
+
+  // APPLY TO STANDARD DROPDOWN ELEMENTS
+  // ===================================
+
+  $(document)
+    .on('click.bs.dropdown.data-api', clearMenus)
+    .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
+    .on('click.bs.dropdown.data-api'  , toggle, Dropdown.prototype.toggle)
+    .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: modal.js v3.0.3
+ * http://getbootstrap.com/javascript/#modals
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // MODAL CLASS DEFINITION
+  // ======================
+
+  var Modal = function (element, options) {
+    this.options   = options
+    this.$element  = $(element)
+    this.$backdrop =
+    this.isShown   = null
+
+    if (this.options.remote) this.$element.load(this.options.remote)
+  }
+
+  Modal.DEFAULTS = {
+      backdrop: true
+    , keyboard: true
+    , show: true
+  }
+
+  Modal.prototype.toggle = function (_relatedTarget) {
+    return this[!this.isShown ? 'show' : 'hide'](_relatedTarget)
+  }
+
+  Modal.prototype.show = function (_relatedTarget) {
+    var that = this
+    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
+
+    this.$element.trigger(e)
+
+    if (this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = true
+
+    this.escape()
+
+    this.$element.on('click.dismiss.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
+
+    this.backdrop(function () {
+      var transition = $.support.transition && that.$element.hasClass('fade')
+
+      if (!that.$element.parent().length) {
+        that.$element.appendTo(document.body) // don't move modals dom position
+      }
+
+      that.$element.show()
+
+      if (transition) {
+        that.$element[0].offsetWidth // force reflow
+      }
+
+      that.$element
+        .addClass('in')
+        .attr('aria-hidden', false)
+
+      that.enforceFocus()
+
+      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
+
+      transition ?
+        that.$element.find('.modal-dialog') // wait for modal to slide in
+          .one($.support.transition.end, function () {
+            that.$element.focus().trigger(e)
+          })
+          .emulateTransitionEnd(300) :
+        that.$element.focus().trigger(e)
+    })
+  }
+
+  Modal.prototype.hide = function (e) {
+    if (e) e.preventDefault()
+
+    e = $.Event('hide.bs.modal')
+
+    this.$element.trigger(e)
+
+    if (!this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = false
+
+    this.escape()
+
+    $(document).off('focusin.bs.modal')
+
+    this.$element
+      .removeClass('in')
+      .attr('aria-hidden', true)
+      .off('click.dismiss.modal')
+
+    $.support.transition && this.$element.hasClass('fade') ?
+      this.$element
+        .one($.support.transition.end, $.proxy(this.hideModal, this))
+        .emulateTransitionEnd(300) :
+      this.hideModal()
+  }
+
+  Modal.prototype.enforceFocus = function () {
+    $(document)
+      .off('focusin.bs.modal') // guard against infinite focus loop
+      .on('focusin.bs.modal', $.proxy(function (e) {
+        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
+          this.$element.focus()
+        }
+      }, this))
+  }
+
+  Modal.prototype.escape = function () {
+    if (this.isShown && this.options.keyboard) {
+      this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) {
+        e.which == 27 && this.hide()
+      }, this))
+    } else if (!this.isShown) {
+      this.$element.off('keyup.dismiss.bs.modal')
+    }
+  }
+
+  Modal.prototype.hideModal = function () {
+    var that = this
+    this.$element.hide()
+    this.backdrop(function () {
+      that.removeBackdrop()
+      that.$element.trigger('hidden.bs.modal')
+    })
+  }
+
+  Modal.prototype.removeBackdrop = function () {
+    this.$backdrop && this.$backdrop.remove()
+    this.$backdrop = null
+  }
+
+  Modal.prototype.backdrop = function (callback) {
+    var that    = this
+    var animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+    if (this.isShown && this.options.backdrop) {
+      var doAnimate = $.support.transition && animate
+
+      this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+        .appendTo(document.body)
+
+      this.$element.on('click.dismiss.modal', $.proxy(function (e) {
+        if (e.target !== e.currentTarget) return
+        this.options.backdrop == 'static'
+          ? this.$element[0].focus.call(this.$element[0])
+          : this.hide.call(this)
+      }, this))
+
+      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+      this.$backdrop.addClass('in')
+
+      if (!callback) return
+
+      doAnimate ?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (!this.isShown && this.$backdrop) {
+      this.$backdrop.removeClass('in')
+
+      $.support.transition && this.$element.hasClass('fade')?
+        this.$backdrop
+          .one($.support.transition.end, callback)
+          .emulateTransitionEnd(150) :
+        callback()
+
+    } else if (callback) {
+      callback()
+    }
+  }
+
+
+  // MODAL PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.modal
+
+  $.fn.modal = function (option, _relatedTarget) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.modal')
+      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
+      if (typeof option == 'string') data[option](_relatedTarget)
+      else if (options.show) data.show(_relatedTarget)
+    })
+  }
+
+  $.fn.modal.Constructor = Modal
+
+
+  // MODAL NO CONFLICT
+  // =================
+
+  $.fn.modal.noConflict = function () {
+    $.fn.modal = old
+    return this
+  }
+
+
+  // MODAL DATA-API
+  // ==============
+
+  $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
+    var $this   = $(this)
+    var href    = $this.attr('href')
+    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
+    var option  = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+
+    e.preventDefault()
+
+    $target
+      .modal(option, this)
+      .one('hide', function () {
+        $this.is(':visible') && $this.focus()
+      })
+  })
+
+  $(document)
+    .on('show.bs.modal',  '.modal', function () { $(document.body).addClass('modal-open') })
+    .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tooltip.js v3.0.3
+ * http://getbootstrap.com/javascript/#tooltip
+ * Inspired by the original jQuery.tipsy by Jason Frame
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // TOOLTIP PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Tooltip = function (element, options) {
+    this.type       =
+    this.options    =
+    this.enabled    =
+    this.timeout    =
+    this.hoverState =
+    this.$element   = null
+
+    this.init('tooltip', element, options)
+  }
+
+  Tooltip.DEFAULTS = {
+    animation: true
+  , placement: 'top'
+  , selector: false
+  , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+  , trigger: 'hover focus'
+  , title: ''
+  , delay: 0
+  , html: false
+  , container: false
+  }
+
+  Tooltip.prototype.init = function (type, element, options) {
+    this.enabled  = true
+    this.type     = type
+    this.$element = $(element)
+    this.options  = this.getOptions(options)
+
+    var triggers = this.options.trigger.split(' ')
+
+    for (var i = triggers.length; i--;) {
+      var trigger = triggers[i]
+
+      if (trigger == 'click') {
+        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+      } else if (trigger != 'manual') {
+        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focus'
+        var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
+
+        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+      }
+    }
+
+    this.options.selector ?
+      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
+      this.fixTitle()
+  }
+
+  Tooltip.prototype.getDefaults = function () {
+    return Tooltip.DEFAULTS
+  }
+
+  Tooltip.prototype.getOptions = function (options) {
+    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+
+    if (options.delay && typeof options.delay == 'number') {
+      options.delay = {
+        show: options.delay
+      , hide: options.delay
+      }
+    }
+
+    return options
+  }
+
+  Tooltip.prototype.getDelegateOptions = function () {
+    var options  = {}
+    var defaults = this.getDefaults()
+
+    this._options && $.each(this._options, function (key, value) {
+      if (defaults[key] != value) options[key] = value
+    })
+
+    return options
+  }
+
+  Tooltip.prototype.enter = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'in'
+
+    if (!self.options.delay || !self.options.delay.show) return self.show()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'in') self.show()
+    }, self.options.delay.show)
+  }
+
+  Tooltip.prototype.leave = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'out'
+
+    if (!self.options.delay || !self.options.delay.hide) return self.hide()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'out') self.hide()
+    }, self.options.delay.hide)
+  }
+
+  Tooltip.prototype.show = function () {
+    var e = $.Event('show.bs.'+ this.type)
+
+    if (this.hasContent() && this.enabled) {
+      this.$element.trigger(e)
+
+      if (e.isDefaultPrevented()) return
+
+      var $tip = this.tip()
+
+      this.setContent()
+
+      if (this.options.animation) $tip.addClass('fade')
+
+      var placement = typeof this.options.placement == 'function' ?
+        this.options.placement.call(this, $tip[0], this.$element[0]) :
+        this.options.placement
+
+      var autoToken = /\s?auto?\s?/i
+      var autoPlace = autoToken.test(placement)
+      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
+
+      $tip
+        .detach()
+        .css({ top: 0, left: 0, display: 'block' })
+        .addClass(placement)
+
+      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+
+      var pos          = this.getPosition()
+      var actualWidth  = $tip[0].offsetWidth
+      var actualHeight = $tip[0].offsetHeight
+
+      if (autoPlace) {
+        var $parent = this.$element.parent()
+
+        var orgPlacement = placement
+        var docScroll    = document.documentElement.scrollTop || document.body.scrollTop
+        var parentWidth  = this.options.container == 'body' ? window.innerWidth  : $parent.outerWidth()
+        var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
+        var parentLeft   = this.options.container == 'body' ? 0 : $parent.offset().left
+
+        placement = placement == 'bottom' && pos.top   + pos.height  + actualHeight - docScroll > parentHeight  ? 'top'    :
+                    placement == 'top'    && pos.top   - docScroll   - actualHeight < 0                         ? 'bottom' :
+                    placement == 'right'  && pos.right + actualWidth > parentWidth                              ? 'left'   :
+                    placement == 'left'   && pos.left  - actualWidth < parentLeft                               ? 'right'  :
+                    placement
+
+        $tip
+          .removeClass(orgPlacement)
+          .addClass(placement)
+      }
+
+      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
+
+      this.applyPlacement(calculatedOffset, placement)
+      this.$element.trigger('shown.bs.' + this.type)
+    }
+  }
+
+  Tooltip.prototype.applyPlacement = function(offset, placement) {
+    var replace
+    var $tip   = this.tip()
+    var width  = $tip[0].offsetWidth
+    var height = $tip[0].offsetHeight
+
+    // manually read margins because getBoundingClientRect includes difference
+    var marginTop = parseInt($tip.css('margin-top'), 10)
+    var marginLeft = parseInt($tip.css('margin-left'), 10)
+
+    // we must check for NaN for ie 8/9
+    if (isNaN(marginTop))  marginTop  = 0
+    if (isNaN(marginLeft)) marginLeft = 0
+
+    offset.top  = offset.top  + marginTop
+    offset.left = offset.left + marginLeft
+
+    $tip
+      .offset(offset)
+      .addClass('in')
+
+    // check to see if placing tip in new offset caused the tip to resize itself
+    var actualWidth  = $tip[0].offsetWidth
+    var actualHeight = $tip[0].offsetHeight
+
+    if (placement == 'top' && actualHeight != height) {
+      replace = true
+      offset.top = offset.top + height - actualHeight
+    }
+
+    if (/bottom|top/.test(placement)) {
+      var delta = 0
+
+      if (offset.left < 0) {
+        delta       = offset.left * -2
+        offset.left = 0
+
+        $tip.offset(offset)
+
+        actualWidth  = $tip[0].offsetWidth
+        actualHeight = $tip[0].offsetHeight
+      }
+
+      this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
+    } else {
+      this.replaceArrow(actualHeight - height, actualHeight, 'top')
+    }
+
+    if (replace) $tip.offset(offset)
+  }
+
+  Tooltip.prototype.replaceArrow = function(delta, dimension, position) {
+    this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
+  }
+
+  Tooltip.prototype.setContent = function () {
+    var $tip  = this.tip()
+    var title = this.getTitle()
+
+    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    $tip.removeClass('fade in top bottom left right')
+  }
+
+  Tooltip.prototype.hide = function () {
+    var that = this
+    var $tip = this.tip()
+    var e    = $.Event('hide.bs.' + this.type)
+
+    function complete() {
+      if (that.hoverState != 'in') $tip.detach()
+    }
+
+    this.$element.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    $tip.removeClass('in')
+
+    $.support.transition && this.$tip.hasClass('fade') ?
+      $tip
+        .one($.support.transition.end, complete)
+        .emulateTransitionEnd(150) :
+      complete()
+
+    this.$element.trigger('hidden.bs.' + this.type)
+
+    return this
+  }
+
+  Tooltip.prototype.fixTitle = function () {
+    var $e = this.$element
+    if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
+    }
+  }
+
+  Tooltip.prototype.hasContent = function () {
+    return this.getTitle()
+  }
+
+  Tooltip.prototype.getPosition = function () {
+    var el = this.$element[0]
+    return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+      width: el.offsetWidth
+    , height: el.offsetHeight
+    }, this.$element.offset())
+  }
+
+  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
+    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
+           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
+        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+  }
+
+  Tooltip.prototype.getTitle = function () {
+    var title
+    var $e = this.$element
+    var o  = this.options
+
+    title = $e.attr('data-original-title')
+      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
+
+    return title
+  }
+
+  Tooltip.prototype.tip = function () {
+    return this.$tip = this.$tip || $(this.options.template)
+  }
+
+  Tooltip.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
+  }
+
+  Tooltip.prototype.validate = function () {
+    if (!this.$element[0].parentNode) {
+      this.hide()
+      this.$element = null
+      this.options  = null
+    }
+  }
+
+  Tooltip.prototype.enable = function () {
+    this.enabled = true
+  }
+
+  Tooltip.prototype.disable = function () {
+    this.enabled = false
+  }
+
+  Tooltip.prototype.toggleEnabled = function () {
+    this.enabled = !this.enabled
+  }
+
+  Tooltip.prototype.toggle = function (e) {
+    var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
+    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
+  }
+
+  Tooltip.prototype.destroy = function () {
+    this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
+  }
+
+
+  // TOOLTIP PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.tooltip
+
+  $.fn.tooltip = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.tooltip')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tooltip.Constructor = Tooltip
+
+
+  // TOOLTIP NO CONFLICT
+  // ===================
+
+  $.fn.tooltip.noConflict = function () {
+    $.fn.tooltip = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: popover.js v3.0.3
+ * http://getbootstrap.com/javascript/#popovers
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // POPOVER PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Popover = function (element, options) {
+    this.init('popover', element, options)
+  }
+
+  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
+
+  Popover.DEFAULTS = $.extend({} , $.fn.tooltip.Constructor.DEFAULTS, {
+    placement: 'right'
+  , trigger: 'click'
+  , content: ''
+  , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
+  })
+
+
+  // NOTE: POPOVER EXTENDS tooltip.js
+  // ================================
+
+  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
+
+  Popover.prototype.constructor = Popover
+
+  Popover.prototype.getDefaults = function () {
+    return Popover.DEFAULTS
+  }
+
+  Popover.prototype.setContent = function () {
+    var $tip    = this.tip()
+    var title   = this.getTitle()
+    var content = this.getContent()
+
+    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
+    $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
+
+    $tip.removeClass('fade top bottom left right in')
+
+    // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
+    // this manually by checking the contents.
+    if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
+  }
+
+  Popover.prototype.hasContent = function () {
+    return this.getTitle() || this.getContent()
+  }
+
+  Popover.prototype.getContent = function () {
+    var $e = this.$element
+    var o  = this.options
+
+    return $e.attr('data-content')
+      || (typeof o.content == 'function' ?
+            o.content.call($e[0]) :
+            o.content)
+  }
+
+  Popover.prototype.arrow = function () {
+    return this.$arrow = this.$arrow || this.tip().find('.arrow')
+  }
+
+  Popover.prototype.tip = function () {
+    if (!this.$tip) this.$tip = $(this.options.template)
+    return this.$tip
+  }
+
+
+  // POPOVER PLUGIN DEFINITION
+  // =========================
+
+  var old = $.fn.popover
+
+  $.fn.popover = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.popover')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.popover.Constructor = Popover
+
+
+  // POPOVER NO CONFLICT
+  // ===================
+
+  $.fn.popover.noConflict = function () {
+    $.fn.popover = old
+    return this
+  }
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: scrollspy.js v3.0.3
+ * http://getbootstrap.com/javascript/#scrollspy
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // SCROLLSPY CLASS DEFINITION
+  // ==========================
+
+  function ScrollSpy(element, options) {
+    var href
+    var process  = $.proxy(this.process, this)
+
+    this.$element       = $(element).is('body') ? $(window) : $(element)
+    this.$body          = $('body')
+    this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process)
+    this.options        = $.extend({}, ScrollSpy.DEFAULTS, options)
+    this.selector       = (this.options.target
+      || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
+      || '') + ' .nav li > a'
+    this.offsets        = $([])
+    this.targets        = $([])
+    this.activeTarget   = null
+
+    this.refresh()
+    this.process()
+  }
+
+  ScrollSpy.DEFAULTS = {
+    offset: 10
+  }
+
+  ScrollSpy.prototype.refresh = function () {
+    var offsetMethod = this.$element[0] == window ? 'offset' : 'position'
+
+    this.offsets = $([])
+    this.targets = $([])
+
+    var self     = this
+    var $targets = this.$body
+      .find(this.selector)
+      .map(function () {
+        var $el   = $(this)
+        var href  = $el.data('target') || $el.attr('href')
+        var $href = /^#\w/.test(href) && $(href)
+
+        return ($href
+          && $href.length
+          && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null
+      })
+      .sort(function (a, b) { return a[0] - b[0] })
+      .each(function () {
+        self.offsets.push(this[0])
+        self.targets.push(this[1])
+      })
+  }
+
+  ScrollSpy.prototype.process = function () {
+    var scrollTop    = this.$scrollElement.scrollTop() + this.options.offset
+    var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
+    var maxScroll    = scrollHeight - this.$scrollElement.height()
+    var offsets      = this.offsets
+    var targets      = this.targets
+    var activeTarget = this.activeTarget
+    var i
+
+    if (scrollTop >= maxScroll) {
+      return activeTarget != (i = targets.last()[0]) && this.activate(i)
+    }
+
+    for (i = offsets.length; i--;) {
+      activeTarget != targets[i]
+        && scrollTop >= offsets[i]
+        && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
+        && this.activate( targets[i] )
+    }
+  }
+
+  ScrollSpy.prototype.activate = function (target) {
+    this.activeTarget = target
+
+    $(this.selector)
+      .parents('.active')
+      .removeClass('active')
+
+    var selector = this.selector
+      + '[data-target="' + target + '"],'
+      + this.selector + '[href="' + target + '"]'
+
+    var active = $(selector)
+      .parents('li')
+      .addClass('active')
+
+    if (active.parent('.dropdown-menu').length)  {
+      active = active
+        .closest('li.dropdown')
+        .addClass('active')
+    }
+
+    active.trigger('activate.bs.scrollspy')
+  }
+
+
+  // SCROLLSPY PLUGIN DEFINITION
+  // ===========================
+
+  var old = $.fn.scrollspy
+
+  $.fn.scrollspy = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.scrollspy')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.scrollspy.Constructor = ScrollSpy
+
+
+  // SCROLLSPY NO CONFLICT
+  // =====================
+
+  $.fn.scrollspy.noConflict = function () {
+    $.fn.scrollspy = old
+    return this
+  }
+
+
+  // SCROLLSPY DATA-API
+  // ==================
+
+  $(window).on('load', function () {
+    $('[data-spy="scroll"]').each(function () {
+      var $spy = $(this)
+      $spy.scrollspy($spy.data())
+    })
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: tab.js v3.0.3
+ * http://getbootstrap.com/javascript/#tabs
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // TAB CLASS DEFINITION
+  // ====================
+
+  var Tab = function (element) {
+    this.element = $(element)
+  }
+
+  Tab.prototype.show = function () {
+    var $this    = this.element
+    var $ul      = $this.closest('ul:not(.dropdown-menu)')
+    var selector = $this.data('target')
+
+    if (!selector) {
+      selector = $this.attr('href')
+      selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
+    }
+
+    if ($this.parent('li').hasClass('active')) return
+
+    var previous = $ul.find('.active:last a')[0]
+    var e        = $.Event('show.bs.tab', {
+      relatedTarget: previous
+    })
+
+    $this.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    var $target = $(selector)
+
+    this.activate($this.parent('li'), $ul)
+    this.activate($target, $target.parent(), function () {
+      $this.trigger({
+        type: 'shown.bs.tab'
+      , relatedTarget: previous
+      })
+    })
+  }
+
+  Tab.prototype.activate = function (element, container, callback) {
+    var $active    = container.find('> .active')
+    var transition = callback
+      && $.support.transition
+      && $active.hasClass('fade')
+
+    function next() {
+      $active
+        .removeClass('active')
+        .find('> .dropdown-menu > .active')
+        .removeClass('active')
+
+      element.addClass('active')
+
+      if (transition) {
+        element[0].offsetWidth // reflow for transition
+        element.addClass('in')
+      } else {
+        element.removeClass('fade')
+      }
+
+      if (element.parent('.dropdown-menu')) {
+        element.closest('li.dropdown').addClass('active')
+      }
+
+      callback && callback()
+    }
+
+    transition ?
+      $active
+        .one($.support.transition.end, next)
+        .emulateTransitionEnd(150) :
+      next()
+
+    $active.removeClass('in')
+  }
+
+
+  // TAB PLUGIN DEFINITION
+  // =====================
+
+  var old = $.fn.tab
+
+  $.fn.tab = function ( option ) {
+    return this.each(function () {
+      var $this = $(this)
+      var data  = $this.data('bs.tab')
+
+      if (!data) $this.data('bs.tab', (data = new Tab(this)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.tab.Constructor = Tab
+
+
+  // TAB NO CONFLICT
+  // ===============
+
+  $.fn.tab.noConflict = function () {
+    $.fn.tab = old
+    return this
+  }
+
+
+  // TAB DATA-API
+  // ============
+
+  $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
+    e.preventDefault()
+    $(this).tab('show')
+  })
+
+}(jQuery);
+
+/* ========================================================================
+ * Bootstrap: affix.js v3.0.3
+ * http://getbootstrap.com/javascript/#affix
+ * ========================================================================
+ * Copyright 2013 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ======================================================================== */
+
+
++function ($) { "use strict";
+
+  // AFFIX CLASS DEFINITION
+  // ======================
+
+  var Affix = function (element, options) {
+    this.options = $.extend({}, Affix.DEFAULTS, options)
+    this.$window = $(window)
+      .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this))
+      .on('click.bs.affix.data-api',  $.proxy(this.checkPositionWithEventLoop, this))
+
+    this.$element = $(element)
+    this.affixed  =
+    this.unpin    = null
+
+    this.checkPosition()
+  }
+
+  Affix.RESET = 'affix affix-top affix-bottom'
+
+  Affix.DEFAULTS = {
+    offset: 0
+  }
+
+  Affix.prototype.checkPositionWithEventLoop = function () {
+    setTimeout($.proxy(this.checkPosition, this), 1)
+  }
+
+  Affix.prototype.checkPosition = function () {
+    if (!this.$element.is(':visible')) return
+
+    var scrollHeight = $(document).height()
+    var scrollTop    = this.$window.scrollTop()
+    var position     = this.$element.offset()
+    var offset       = this.options.offset
+    var offsetTop    = offset.top
+    var offsetBottom = offset.bottom
+
+    if (typeof offset != 'object')         offsetBottom = offsetTop = offset
+    if (typeof offsetTop == 'function')    offsetTop    = offset.top()
+    if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
+
+    var affix = this.unpin   != null && (scrollTop + this.unpin <= position.top) ? false :
+                offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' :
+                offsetTop    != null && (scrollTop <= offsetTop) ? 'top' : false
+
+    if (this.affixed === affix) return
+    if (this.unpin) this.$element.css('top', '')
+
+    this.affixed = affix
+    this.unpin   = affix == 'bottom' ? position.top - scrollTop : null
+
+    this.$element.removeClass(Affix.RESET).addClass('affix' + (affix ? '-' + affix : ''))
+
+    if (affix == 'bottom') {
+      this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() })
+    }
+  }
+
+
+  // AFFIX PLUGIN DEFINITION
+  // =======================
+
+  var old = $.fn.affix
+
+  $.fn.affix = function (option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.affix')
+      var options = typeof option == 'object' && option
+
+      if (!data) $this.data('bs.affix', (data = new Affix(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  $.fn.affix.Constructor = Affix
+
+
+  // AFFIX NO CONFLICT
+  // =================
+
+  $.fn.affix.noConflict = function () {
+    $.fn.affix = old
+    return this
+  }
+
+
+  // AFFIX DATA-API
+  // ==============
+
+  $(window).on('load', function () {
+    $('[data-spy="affix"]').each(function () {
+      var $spy = $(this)
+      var data = $spy.data()
+
+      data.offset = data.offset || {}
+
+      if (data.offsetBottom) data.offset.bottom = data.offsetBottom
+      if (data.offsetTop)    data.offset.top    = data.offsetTop
+
+      $spy.affix(data)
+    })
+  })
+
+}(jQuery);
Index: main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.min.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.min.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/bootstrap/js/bootstrap.min.js	(revision 16045)
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.0.3 (http://getbootstrap.com)
+ * Copyright 2013 Twitter, Inc.
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
Index: main/waeup.kofa/branches/uli-py3/layout/static/css/IE8.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/css/IE8.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/css/IE8.css	(revision 16045)
@@ -0,0 +1,8 @@
+body {
+    padding-top: 60px;
+}
+
+ .navbar-brand {
+    padding-top:15px;
+    padding-bottom:15px;
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/css/base.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/css/base.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/css/base.css	(revision 16045)
@@ -0,0 +1,372 @@
+/* Customized Bootstrap styles */
+
+body {
+    background-color: #f5f5f5;
+    padding-top: 52px;
+}
+
+.navbar-fixed-top, .navbar {
+    min-height:40px;
+}
+
+.navbar-nav > li > a, .navbar-brand {
+    padding-top:10px;
+    padding-bottom:10px;
+}
+
+.navbar-toggle {
+    padding: 4px 10px;
+}
+
+.navbar-nav.navbar-right:last-child {
+  margin-right: -5px;
+}
+
+.navbar-inverse {
+  border-color: #FFFFFF;
+}
+
+.kofa-langselect img {
+    margin-top: -10px;
+    margin-bottom: -10px;
+}
+
+.breadcrumb , .wfstatus, .maintenance-warning {
+    background-color: #fff;
+    margin-bottom: 0px;
+    margin-top: 0px;
+    padding: 2px 15px;
+}
+
+.breadcrumb > li {
+    display: inline;
+}
+
+.breadcrumb > li + li:before {
+    content: "/";
+    padding: 0px;
+}
+
+.well {
+    padding: 0px 5px 20px 5px;
+    margin-bottom: 10px;
+    background-image: None;
+    background-color: #fff;
+    filter: None;
+}
+
+.alert {
+  margin-bottom: 10px;
+  margin-top: 10px;
+}
+
+
+/* Kofa styles */
+
+.kofa-sidebar,
+.kofa-content-narrow,
+.kofa-content-wide,
+.kofa-breadcrumbs-box-short,
+.kofa-breadcrumbs-box-long,
+.kofa-student-status-box,
+.kofa-maintenance {
+    padding: 0px 5px 0px 5px;
+}
+
+.workflow {
+    border:1px solid #1fdf00;
+    background-color:#bbffb6;
+    font-family: courier;
+    font-size: 100%;
+    padding: 4px;
+    margin-bottom: 8px;
+    line-height: 16px;
+}
+
+.kofa-breadcrumbs-box-short .well,
+.kofa-breadcrumbs-box-long .well,
+.kofa-student-status-box .well,
+.kofa-maintenance .well {
+    padding: 5px 5px 5px 5px;
+}
+
+.kofa-content-wide .well, .kofa-content-narrow .well {
+    padding: 10px 20px 10px 20px;
+}
+
+.kofa-content-desc {
+    color: #BFBFBF;
+    font-size: 18px;
+}
+
+.wfstatus, .wfstatus-sub {
+    text-align: center;
+    background-color: #909090;
+    color: white;
+}
+
+.kofa-student-status-box .well {
+    background-color: #909090;
+}
+
+.maintenance-warning {
+    text-align: center;
+}
+
+.kofa-maintenance .well {
+    background-color: red;
+}
+
+.separator {
+    font-size: 16px;
+    color: #404040;
+    font-weight: bold;
+    line-height: 36px;
+}
+
+/* Sidebar */
+
+.kofa-sidebar-title {
+    text-transform: none;
+    font-size: 14px;
+    color: #555;
+    font-weight: bold;
+    padding: 15px 0px 10px 8px;
+}
+
+.sidebar-nav .nav  > li > a {
+    padding: 3px 15px;
+}
+
+/* Buttons */
+
+input.btn {
+    padding: 6px 12px;
+}
+
+input.btn-sm {
+  padding: 5px 10px;
+}
+
+input.btn-xs {
+  padding: 1px 10px;
+}
+
+.table-condensed .btn {
+    margin: 0px;
+    font-size: 12px;
+}
+
+
+/* Content actionbar */
+
+div.actionbar {
+    margin-top: -6px;
+    margin-left: -10px;
+    margin-bottom: 0px;
+}
+
+.actionbar .btn-default {
+    margin-bottom: 4px;
+    padding: 4px 8px;
+    margin: 0px 0px 4px 0px;
+    display: inline-block;
+}
+
+/* Documentation link*/
+
+div.documentation {
+    margin-top: -20px;
+    text-align: right;
+}
+
+/* Kofa forms */
+
+.form-table {
+    width: 100%;
+}
+
+.form-table td {
+    padding: 5px 0px 5px 4px;
+    border-top: none;
+}
+
+.value select, .phone-input, .ac-input,
+.textType, textarea,
+.dataTables_filter input,
+.dataTables_length select,
+.ordered-selection-field select {
+    background-color: #FFFFFF;
+    border: 1px solid #CCCCCC;
+    border-radius: 4px;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
+    color: #555555;
+    padding: 6px 12px;
+    transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s;
+    margin: 0px 4px 0px 0px;
+    width: 100%;
+}
+
+.dataTables_length select {
+    width: 90px;
+}
+
+.value select:focus, .phone-input:focus, .ac-input:focus,
+.textType:focus, textarea:focus,
+.dataTables_filter input:focus,
+.ordered-selection-field select:focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+          box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.value select, .half {
+    width: 50%;
+}
+
+textarea {
+    font-family: Courier,"Courier New",monospace;
+}
+
+.fieldname {
+    width: 25%;
+}
+
+.form-table td {
+    vertical-align: bottom;
+}
+
+.form-table td.fieldname {
+    vertical-align: top;
+}
+
+.form-inline select {
+    width: 100%;
+}
+
+.phone-input {
+    width: 30%;
+}
+
+.ac-series {
+    width: 50px;
+}
+
+.ac-number {
+    width: 110px;
+}
+
+.hint {
+    padding: 0px 0px 5px 0px;
+    color: #bfbfbf;
+}
+
+.error {
+    color: red;
+}
+
+.datepicker-le-year {
+    width: 50%;
+}
+
+.datepicker-short {
+    width: 15%;
+}
+
+.ui-widget-header .ui-icon {
+    background-image: url("../img/ui-icons_222222_256x240.png");
+}
+
+
+ /* dataTables stuff */
+
+.dataTables_wrapper {
+    min-height: 50px;
+    margin-bottom: 50px;
+}
+
+.dataTables_filter input {
+    width: 60%;
+}
+
+.sorting { background: url('../img/sort_both.png') no-repeat center right; }
+.sorting_asc { background: url('../img/sort_asc.png') no-repeat center right; }
+.sorting_desc { background: url('../img/sort_desc.png') no-repeat center right; }
+
+.sorting_asc_disabled { background: url('../img/sort_asc_disabled.png') no-repeat center right; }
+.sorting_desc_disabled { background: url('../img/sort_desc_disabled.png') no-repeat center right; }
+
+.paginate_enabled_previous { background: url('../img/back_enabled.png') no-repeat top left; }
+.paginate_enabled_previous:hover { background: url('../img/back_enabled_hover.png') no-repeat top left; }
+.paginate_disabled_previous { background: url('../img/back_disabled.png') no-repeat top left; }
+
+.paginate_enabled_next { background: url('../img/forward_enabled.png') no-repeat top right; }
+.paginate_enabled_next:hover { background: url('../img/forward_enabled_hover.png') no-repeat top right; }
+.paginate_disabled_next { background: url('../img/forward_disabled.png') no-repeat top right; }
+
+/* Scroll icons */
+
+#toTop-right {
+	display:none;
+	text-decoration:none;
+	position:fixed;
+	bottom:-4px;
+	right:-4px;
+	outline:none;
+}
+
+.scroll-up {
+  background: url("../img/scroll_up.png") no-repeat scroll left top rgba(0, 0, 0, 0);
+  padding: 25px;
+}
+
+#toBottom-right {
+	display:none;
+	text-decoration:none;
+	position:fixed;
+	top:47px;
+	right:-4px;
+	outline:none;
+}
+
+.scroll-down {
+  background: url("../img/scroll_down.png") no-repeat scroll left top rgba(0, 0, 0, 0);
+  padding: 25px;
+}
+
+ /* docutils stuff */
+
+.section h1 {
+    font-size: 30px}
+
+.section h2 {
+    font-size: 24px}
+
+.section h3 {
+    font-size: 18px}
+
+.section h4 {
+    font-size: 14px}
+
+/* Smartphone styles */
+
+@media (max-width: 767px) {
+
+  .kofa-sidebar,
+  .kofa-content-narrow ,
+  .kofa-breadcrumbs-box-short,
+  .kofa-breadcrumbs-box-long,
+  .kofa-student-status-box {
+      padding: 0px 0px 0px 0px;
+  }
+
+  .kofa-content-wide {
+      padding: 0px 0px 0px 0px;
+  }
+
+  .value select, .half {
+      width: 100%;
+  }
+
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/css/file-upload.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/css/file-upload.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/css/file-upload.css	(revision 16045)
@@ -0,0 +1,19 @@
+.btn-file {
+    position: relative;
+    overflow: hidden;
+}
+.btn-file input[type=file] {
+    position: absolute;
+    top: 0;
+    right: 0;
+    min-width: 100%;
+    min-height: 100%;
+    font-size: 999px;
+    text-align: right;
+    filter: alpha(opacity=0);
+    opacity: 0;
+    outline: none;
+    background: white;
+    cursor: inherit;
+    display: block;
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/css/jquery-ui.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/css/jquery-ui.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/css/jquery-ui.css	(revision 16045)
@@ -0,0 +1,1229 @@
+/*! jQuery UI - v1.11.0pre - 2014-01-29
+* http://jqueryui.com
+* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+	display: none;
+}
+.ui-helper-hidden-accessible {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+}
+.ui-helper-reset {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	outline: 0;
+	line-height: 1.3;
+	text-decoration: none;
+	font-size: 100%;
+	list-style: none;
+}
+.ui-helper-clearfix:before,
+.ui-helper-clearfix:after {
+	content: "";
+	display: table;
+	border-collapse: collapse;
+}
+.ui-helper-clearfix:after {
+	clear: both;
+}
+.ui-helper-clearfix {
+	min-height: 0; /* support: IE7 */
+}
+.ui-helper-zfix {
+	width: 100%;
+	height: 100%;
+	top: 0;
+	left: 0;
+	position: absolute;
+	opacity: 0;
+	filter:Alpha(Opacity=0);
+}
+
+.ui-front {
+	z-index: 100;
+}
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled {
+	cursor: default !important;
+}
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	display: block;
+	text-indent: -99999px;
+	overflow: hidden;
+	background-repeat: no-repeat;
+}
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+
+.ui-accordion .ui-accordion-header {
+	display: block;
+	cursor: pointer;
+	position: relative;
+	margin: 2px 0 0 0;
+	padding: .5em .5em .5em .7em;
+	min-height: 0; /* support: IE7 */
+	font-size: 100%;
+}
+.ui-accordion .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-icons .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
+	position: absolute;
+	left: .5em;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-accordion .ui-accordion-content {
+	padding: 1em 2.2em;
+	border-top: 0;
+	overflow: auto;
+}
+
+.ui-autocomplete {
+	position: absolute;
+	top: 0;
+	left: 0;
+	cursor: default;
+}
+
+.ui-button {
+	display: inline-block;
+	position: relative;
+	padding: 0;
+	line-height: normal;
+	margin-right: .1em;
+	cursor: pointer;
+	vertical-align: middle;
+	text-align: center;
+	overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+	text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+	width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+	width: 2.4em;
+}
+.ui-button-icons-only {
+	width: 3.4em;
+}
+button.ui-button-icons-only {
+	width: 3.7em;
+}
+
+/* button text element */
+.ui-button .ui-button-text {
+	display: block;
+	line-height: normal;
+}
+.ui-button-text-only .ui-button-text {
+	padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+	padding: .4em;
+	text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+	padding-left: 2.1em;
+	padding-right: 2.1em;
+}
+/* no icon support for input elements, provide padding by default */
+input.ui-button {
+	padding: .4em 1em;
+}
+
+/* button icon element(s) */
+.ui-button-icon-only .ui-icon,
+.ui-button-text-icon-primary .ui-icon,
+.ui-button-text-icon-secondary .ui-icon,
+.ui-button-text-icons .ui-icon,
+.ui-button-icons-only .ui-icon {
+	position: absolute;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-button-icon-only .ui-icon {
+	left: 50%;
+	margin-left: -8px;
+}
+.ui-button-text-icon-primary .ui-button-icon-primary,
+.ui-button-text-icons .ui-button-icon-primary,
+.ui-button-icons-only .ui-button-icon-primary {
+	left: .5em;
+}
+.ui-button-text-icon-secondary .ui-button-icon-secondary,
+.ui-button-text-icons .ui-button-icon-secondary,
+.ui-button-icons-only .ui-button-icon-secondary {
+	right: .5em;
+}
+
+/* button sets */
+.ui-buttonset {
+	margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+	margin-left: 0;
+	margin-right: -.3em;
+}
+
+/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+	position: absolute;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
+}
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.ui-datepicker .ui-datepicker-prev span,
+.ui-datepicker .ui-datepicker-next span {
+	display: block;
+	position: absolute;
+	left: 50%;
+	margin-left: -8px;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 49%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.ui-datepicker .ui-datepicker-buttonpane {
+	background-image: none;
+	margin: .7em 0 0 0;
+	padding: 0 .2em;
+	border-left: 0;
+	border-right: 0;
+	border-bottom: 0;
+}
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
+}
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
+}
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
+}
+
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
+}
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+
+.ui-dialog {
+	overflow: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+	padding: .2em;
+	outline: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+	padding: .4em 1em;
+	position: relative;
+}
+.ui-dialog .ui-dialog-title {
+	float: left;
+	margin: .1em 0;
+	white-space: nowrap;
+	width: 90%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+	position: absolute;
+	right: .3em;
+	top: 50%;
+	width: 20px;
+	margin: -10px 0 0 0;
+	padding: 1px;
+	height: 20px;
+}
+.ui-dialog .ui-dialog-content {
+	position: relative;
+	border: 0;
+	padding: .5em 1em;
+	background: none;
+	overflow: auto;
+}
+.ui-dialog .ui-dialog-buttonpane {
+	text-align: left;
+	border-width: 1px 0 0 0;
+	background-image: none;
+	margin-top: .5em;
+	padding: .3em 1em .5em .4em;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+	float: right;
+}
+.ui-dialog .ui-dialog-buttonpane button {
+	margin: .5em .4em .5em 0;
+	cursor: pointer;
+}
+.ui-dialog .ui-resizable-se {
+	width: 12px;
+	height: 12px;
+	right: -5px;
+	bottom: -5px;
+	background-position: 16px 16px;
+}
+.ui-draggable .ui-dialog-titlebar {
+	cursor: move;
+}
+
+.ui-menu {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	display: block;
+	outline: none;
+}
+.ui-menu .ui-menu {
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	margin: 0;
+	display: block;
+	padding: 3px .4em;
+	cursor: pointer;
+	min-height: 0; /* support: IE7 */
+	/* support: IE10, see #8844 */
+	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px 0;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-state-focus,
+.ui-menu .ui-state-active {
+	margin: -1px;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item {
+	position: relative;
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: .2em;
+	left: .2em;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	position: relative;
+	top: -.2em;
+	float: right;
+}
+
+.ui-progressbar {
+	height: 2em;
+	text-align: left;
+	overflow: hidden;
+}
+.ui-progressbar .ui-progressbar-value {
+	margin: -1px;
+	height: 100%;
+}
+.ui-progressbar .ui-progressbar-overlay {
+	background: url("images/animated-overlay.gif");
+	height: 100%;
+	filter: alpha(opacity=25);
+	opacity: 0.25;
+}
+.ui-progressbar-indeterminate .ui-progressbar-value {
+	background-image: none;
+}
+
+.ui-resizable {
+	position: relative;
+}
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
+}
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
+}
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
+}
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
+}
+
+.ui-selectable {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
+
+.ui-selectmenu-menu {
+	padding: 0;
+	margin: 0;
+	position: absolute;
+	top: 0;
+	left: 0;
+	display: none;
+}
+.ui-selectmenu-menu .ui-menu {
+	overflow: auto;
+	/* Support: IE7 */
+	overflow-x: hidden;
+	padding-bottom: 1px;
+}
+.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
+	font-size: 1em;
+	font-weight: bold;
+	line-height: 1.5;
+	padding: 2px 0.4em;
+	margin: 0.5em 0 0 0;
+	height: auto;
+	border: 0;
+}
+.ui-selectmenu-open {
+	display: block;
+}
+.ui-selectmenu-button {
+	display: inline-block;
+	overflow: hidden;
+	position: relative;
+	text-decoration: none;
+	cursor: pointer;
+}
+.ui-selectmenu-button span.ui-icon {
+	right: 0.5em;
+	left: auto;
+	margin-top: -8px;
+	position: absolute;
+	top: 50%;
+}
+.ui-selectmenu-button span.ui-selectmenu-text {
+	text-align: left;
+	padding: 0.4em 2.1em 0.4em 1em;
+	display: block;
+	line-height: 1.4;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.ui-slider {
+	position: relative;
+	text-align: left;
+}
+.ui-slider .ui-slider-handle {
+	position: absolute;
+	z-index: 2;
+	width: 1.2em;
+	height: 1.2em;
+	cursor: default;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-slider .ui-slider-range {
+	position: absolute;
+	z-index: 1;
+	font-size: .7em;
+	display: block;
+	border: 0;
+	background-position: 0 0;
+}
+
+/* For IE8 - See #6727 */
+.ui-slider.ui-state-disabled .ui-slider-handle,
+.ui-slider.ui-state-disabled .ui-slider-range {
+	filter: inherit;
+}
+
+.ui-slider-horizontal {
+	height: .8em;
+}
+.ui-slider-horizontal .ui-slider-handle {
+	top: -.3em;
+	margin-left: -.6em;
+}
+.ui-slider-horizontal .ui-slider-range {
+	top: 0;
+	height: 100%;
+}
+.ui-slider-horizontal .ui-slider-range-min {
+	left: 0;
+}
+.ui-slider-horizontal .ui-slider-range-max {
+	right: 0;
+}
+
+.ui-slider-vertical {
+	width: .8em;
+	height: 100px;
+}
+.ui-slider-vertical .ui-slider-handle {
+	left: -.3em;
+	margin-left: 0;
+	margin-bottom: -.6em;
+}
+.ui-slider-vertical .ui-slider-range {
+	left: 0;
+	width: 100%;
+}
+.ui-slider-vertical .ui-slider-range-min {
+	bottom: 0;
+}
+.ui-slider-vertical .ui-slider-range-max {
+	top: 0;
+}
+
+.ui-spinner {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	padding: 0;
+	vertical-align: middle;
+}
+.ui-spinner-input {
+	border: none;
+	background: none;
+	color: inherit;
+	padding: 0;
+	margin: .2em 0;
+	vertical-align: middle;
+	margin-left: .4em;
+	margin-right: 22px;
+}
+.ui-spinner-button {
+	width: 16px;
+	height: 50%;
+	font-size: .5em;
+	padding: 0;
+	margin: 0;
+	text-align: center;
+	position: absolute;
+	cursor: default;
+	display: block;
+	overflow: hidden;
+	right: 0;
+}
+/* more specificity required here to override default borders */
+.ui-spinner a.ui-spinner-button {
+	border-top: none;
+	border-bottom: none;
+	border-right: none;
+}
+/* vertically center icon */
+.ui-spinner .ui-icon {
+	position: absolute;
+	margin-top: -8px;
+	top: 50%;
+	left: 0;
+}
+.ui-spinner-up {
+	top: 0;
+}
+.ui-spinner-down {
+	bottom: 0;
+}
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position: -65px -16px;
+}
+
+.ui-tabs {
+	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+	padding: .2em;
+}
+.ui-tabs .ui-tabs-nav {
+	margin: 0;
+	padding: .2em .2em 0;
+}
+.ui-tabs .ui-tabs-nav li {
+	list-style: none;
+	float: left;
+	position: relative;
+	top: 0;
+	margin: 1px .2em 0 0;
+	border-bottom-width: 0;
+	padding: 0;
+	white-space: nowrap;
+}
+.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+	float: left;
+	padding: .5em 1em;
+	text-decoration: none;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active {
+	margin-bottom: -1px;
+	padding-bottom: 1px;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
+	cursor: text;
+}
+.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
+	cursor: pointer;
+}
+.ui-tabs .ui-tabs-panel {
+	display: block;
+	border-width: 0;
+	padding: 1em 1.4em;
+	background: none;
+}
+
+.ui-tooltip {
+	padding: 8px;
+	position: absolute;
+	z-index: 9999;
+	max-width: 300px;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+body .ui-tooltip {
+	border-width: 2px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget {
+	font-family: Verdana,Arial,sans-serif/*{ffDefault}*/;
+	font-size: 1.1em/*{fsDefault}*/;
+}
+.ui-widget .ui-widget {
+	font-size: 1em;
+}
+.ui-widget input,
+.ui-widget select,
+.ui-widget textarea,
+.ui-widget button {
+	font-family: Verdana,Arial,sans-serif/*{ffDefault}*/;
+	font-size: 1em;
+}
+.ui-widget-content {
+	border: 1px solid #aaaaaa/*{borderColorContent}*/;
+	background: #ffffff/*{bgColorContent}*/ url("images/ui-bg_flat_75_ffffff_40x100.png")/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/;
+	color: #222222/*{fcContent}*/;
+}
+.ui-widget-content a {
+	color: #222222/*{fcContent}*/;
+}
+.ui-widget-header {
+	border: 1px solid #aaaaaa/*{borderColorHeader}*/;
+	background: #cccccc/*{bgColorHeader}*/ url("images/ui-bg_highlight-soft_75_cccccc_1x100.png")/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/;
+	color: #222222/*{fcHeader}*/;
+	font-weight: bold;
+}
+.ui-widget-header a {
+	color: #222222/*{fcHeader}*/;
+}
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default,
+.ui-widget-content .ui-state-default,
+.ui-widget-header .ui-state-default {
+	border: 1px solid #d3d3d3/*{borderColorDefault}*/;
+	background: #e6e6e6/*{bgColorDefault}*/ url("images/ui-bg_glass_75_e6e6e6_1x400.png")/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #555555/*{fcDefault}*/;
+}
+.ui-state-default a,
+.ui-state-default a:link,
+.ui-state-default a:visited {
+	color: #555555/*{fcDefault}*/;
+	text-decoration: none;
+}
+.ui-state-hover,
+.ui-widget-content .ui-state-hover,
+.ui-widget-header .ui-state-hover,
+.ui-state-focus,
+.ui-widget-content .ui-state-focus,
+.ui-widget-header .ui-state-focus {
+	border: 1px solid #999999/*{borderColorHover}*/;
+	background: #dadada/*{bgColorHover}*/ url("images/ui-bg_glass_75_dadada_1x400.png")/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #212121/*{fcHover}*/;
+}
+.ui-state-hover a,
+.ui-state-hover a:hover,
+.ui-state-hover a:link,
+.ui-state-hover a:visited,
+.ui-state-focus a,
+.ui-state-focus a:hover,
+.ui-state-focus a:link,
+.ui-state-focus a:visited {
+	color: #212121/*{fcHover}*/;
+	text-decoration: none;
+}
+.ui-state-active,
+.ui-widget-content .ui-state-active,
+.ui-widget-header .ui-state-active {
+	border: 1px solid #aaaaaa/*{borderColorActive}*/;
+	background: #ffffff/*{bgColorActive}*/ url("images/ui-bg_glass_65_ffffff_1x400.png")/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/;
+	font-weight: normal/*{fwDefault}*/;
+	color: #212121/*{fcActive}*/;
+}
+.ui-state-active a,
+.ui-state-active a:link,
+.ui-state-active a:visited {
+	color: #212121/*{fcActive}*/;
+	text-decoration: none;
+}
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight,
+.ui-widget-content .ui-state-highlight,
+.ui-widget-header .ui-state-highlight {
+	border: 1px solid #fcefa1/*{borderColorHighlight}*/;
+	background: #fbf9ee/*{bgColorHighlight}*/ url("images/ui-bg_glass_55_fbf9ee_1x400.png")/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/;
+	color: #363636/*{fcHighlight}*/;
+}
+.ui-state-highlight a,
+.ui-widget-content .ui-state-highlight a,
+.ui-widget-header .ui-state-highlight a {
+	color: #363636/*{fcHighlight}*/;
+}
+.ui-state-error,
+.ui-widget-content .ui-state-error,
+.ui-widget-header .ui-state-error {
+	border: 1px solid #cd0a0a/*{borderColorError}*/;
+	background: #fef1ec/*{bgColorError}*/ url("images/ui-bg_glass_95_fef1ec_1x400.png")/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/;
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-state-error a,
+.ui-widget-content .ui-state-error a,
+.ui-widget-header .ui-state-error a {
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-state-error-text,
+.ui-widget-content .ui-state-error-text,
+.ui-widget-header .ui-state-error-text {
+	color: #cd0a0a/*{fcError}*/;
+}
+.ui-priority-primary,
+.ui-widget-content .ui-priority-primary,
+.ui-widget-header .ui-priority-primary {
+	font-weight: bold;
+}
+.ui-priority-secondary,
+.ui-widget-content .ui-priority-secondary,
+.ui-widget-header .ui-priority-secondary {
+	opacity: .7;
+	filter:Alpha(Opacity=70);
+	font-weight: normal;
+}
+.ui-state-disabled,
+.ui-widget-content .ui-state-disabled,
+.ui-widget-header .ui-state-disabled {
+	opacity: .35;
+	filter:Alpha(Opacity=35);
+	background-image: none;
+}
+.ui-state-disabled .ui-icon {
+	filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
+}
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	width: 16px;
+	height: 16px;
+}
+.ui-icon,
+.ui-widget-content .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png")/*{iconsContent}*/;
+}
+.ui-widget-header .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png")/*{iconsHeader}*/;
+}
+.ui-state-default .ui-icon {
+	background-image: url("images/ui-icons_888888_256x240.png")/*{iconsDefault}*/;
+}
+.ui-state-hover .ui-icon,
+.ui-state-focus .ui-icon {
+	background-image: url("images/ui-icons_454545_256x240.png")/*{iconsHover}*/;
+}
+.ui-state-active .ui-icon {
+	background-image: url("images/ui-icons_454545_256x240.png")/*{iconsActive}*/;
+}
+.ui-state-highlight .ui-icon {
+	background-image: url("images/ui-icons_2e83ff_256x240.png")/*{iconsHighlight}*/;
+}
+.ui-state-error .ui-icon,
+.ui-state-error-text .ui-icon {
+	background-image: url("images/ui-icons_cd0a0a_256x240.png")/*{iconsError}*/;
+}
+
+/* positioning */
+.ui-icon-blank { background-position: 16px 16px; }
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-left,
+.ui-corner-tl {
+	border-top-left-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-right,
+.ui-corner-tr {
+	border-top-right-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-left,
+.ui-corner-bl {
+	border-bottom-left-radius: 4px/*{cornerRadius}*/;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-right,
+.ui-corner-br {
+	border-bottom-right-radius: 4px/*{cornerRadius}*/;
+}
+
+/* Overlays */
+.ui-widget-overlay {
+	background: #aaaaaa/*{bgColorOverlay}*/ url("images/ui-bg_flat_0_aaaaaa_40x100.png")/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/;
+	opacity: .3/*{opacityOverlay}*/;
+	filter: Alpha(Opacity=30)/*{opacityFilterOverlay}*/;
+}
+.ui-widget-shadow {
+	margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/;
+	padding: 8px/*{thicknessShadow}*/;
+	background: #aaaaaa/*{bgColorShadow}*/ url("images/ui-bg_flat_0_aaaaaa_40x100.png")/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/;
+	opacity: .3/*{opacityShadow}*/;
+	filter: Alpha(Opacity=30)/*{opacityFilterShadow}*/;
+	border-radius: 8px/*{cornerRadiusShadow}*/;
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/css/jquery.dataTables.css
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/css/jquery.dataTables.css	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/css/jquery.dataTables.css	(revision 16045)
@@ -0,0 +1,221 @@
+
+/*
+ * Table
+ */
+table.dataTable {
+	margin: 0 auto;
+	clear: both;
+	width: 100%;
+}
+
+table.dataTable thead th {
+	padding: 3px 18px 3px 10px;
+	border-bottom: 1px solid black;
+	font-weight: bold;
+	cursor: pointer;
+	*cursor: hand;
+}
+
+table.dataTable tfoot th {
+	padding: 3px 18px 3px 10px;
+	border-top: 1px solid black;
+	font-weight: bold;
+}
+
+table.dataTable td {
+	padding: 3px 10px;
+}
+
+table.dataTable td.center,
+table.dataTable td.dataTables_empty {
+	text-align: center;
+}
+
+table.dataTable tr.odd { background-color: #E2E4FF; }
+table.dataTable tr.even { background-color: white; }
+
+table.dataTable tr.odd td.sorting_1 { background-color: #D3D6FF; }
+table.dataTable tr.odd td.sorting_2 { background-color: #DADCFF; }
+table.dataTable tr.odd td.sorting_3 { background-color: #E0E2FF; }
+table.dataTable tr.even td.sorting_1 { background-color: #EAEBFF; }
+table.dataTable tr.even td.sorting_2 { background-color: #F2F3FF; }
+table.dataTable tr.even td.sorting_3 { background-color: #F9F9FF; }
+
+
+/*
+ * Table wrapper
+ */
+.dataTables_wrapper {
+	position: relative;
+	clear: both;
+	*zoom: 1;
+}
+
+
+/*
+ * Page length menu
+ */
+.dataTables_length {
+	float: left;
+}
+
+
+/*
+ * Filter
+ */
+.dataTables_filter {
+	float: right;
+	text-align: right;
+}
+
+
+/*
+ * Table information
+ */
+.dataTables_info {
+	clear: both;
+	float: left;
+}
+
+
+/*
+ * Pagination
+ */
+.dataTables_paginate {
+	float: right;
+	text-align: right;
+}
+
+/* Two button pagination - previous / next */
+.paginate_disabled_previous,
+.paginate_enabled_previous,
+.paginate_disabled_next,
+.paginate_enabled_next {
+	height: 19px;
+	float: left;
+	cursor: pointer;
+	*cursor: hand;
+	color: #111 !important;
+}
+.paginate_disabled_previous:hover,
+.paginate_enabled_previous:hover,
+.paginate_disabled_next:hover,
+.paginate_enabled_next:hover {
+	text-decoration: none !important;
+}
+.paginate_disabled_previous:active,
+.paginate_enabled_previous:active,
+.paginate_disabled_next:active,
+.paginate_enabled_next:active {
+	outline: none;
+}
+
+.paginate_disabled_previous,
+.paginate_disabled_next {
+	color: #666 !important;
+}
+.paginate_disabled_previous,
+.paginate_enabled_previous {
+	padding-left: 23px;
+}
+.paginate_disabled_next,
+.paginate_enabled_next {
+	padding-right: 23px;
+	margin-left: 10px;
+}
+
+.paginate_enabled_previous { background: url('../images/back_enabled.png') no-repeat top left; }
+.paginate_enabled_previous:hover { background: url('../images/back_enabled_hover.png') no-repeat top left; }
+.paginate_disabled_previous { background: url('../images/back_disabled.png') no-repeat top left; }
+
+.paginate_enabled_next { background: url('../images/forward_enabled.png') no-repeat top right; }
+.paginate_enabled_next:hover { background: url('../images/forward_enabled_hover.png') no-repeat top right; }
+.paginate_disabled_next { background: url('../images/forward_disabled.png') no-repeat top right; }
+
+/* Full number pagination */
+.paging_full_numbers {
+	height: 22px;
+	line-height: 22px;
+}
+.paging_full_numbers a:active {
+	outline: none
+}
+.paging_full_numbers a:hover {
+	text-decoration: none;
+}
+
+.paging_full_numbers a.paginate_button,
+.paging_full_numbers a.paginate_active {
+	border: 1px solid #aaa;
+	-webkit-border-radius: 5px;
+	-moz-border-radius: 5px;
+	border-radius: 5px;
+	padding: 2px 5px;
+	margin: 0 3px;
+	cursor: pointer;
+	*cursor: hand;
+	color: #333 !important;
+}
+
+.paging_full_numbers a.paginate_button {
+	background-color: #ddd;
+}
+
+.paging_full_numbers a.paginate_button:hover {
+	background-color: #ccc;
+	text-decoration: none !important;
+}
+
+.paging_full_numbers a.paginate_active {
+	background-color: #99B3FF;
+}
+
+
+/*
+ * Processing indicator
+ */
+.dataTables_processing {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	width: 250px;
+	height: 30px;
+	margin-left: -125px;
+	margin-top: -15px;
+	padding: 14px 0 2px 0;
+	border: 1px solid #ddd;
+	text-align: center;
+	color: #999;
+	font-size: 14px;
+	background-color: white;
+}
+
+
+/*
+ * Sorting
+ */
+.sorting { background: url('../images/sort_both.png') no-repeat center right; }
+.sorting_asc { background: url('../images/sort_asc.png') no-repeat center right; }
+.sorting_desc { background: url('../images/sort_desc.png') no-repeat center right; }
+
+.sorting_asc_disabled { background: url('../images/sort_asc_disabled.png') no-repeat center right; }
+.sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; }
+ 
+table.dataTable thead th:active,
+table.dataTable thead td:active {
+	outline: none;
+}
+
+
+/*
+ * Scrolling
+ */
+.dataTables_scroll {
+	clear: both;
+}
+
+.dataTables_scrollBody {
+	*margin-top: -1px;
+	-webkit-overflow-scrolling: touch;
+}
+
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/file-upload.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/file-upload.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/file-upload.js	(revision 16045)
@@ -0,0 +1,21 @@
+$(document)
+	.on('change', '.btn-file :file', function() {
+		var input = $(this),
+		numFiles = input.get(0).files ? input.get(0).files.length : 1,
+		label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
+		input.trigger('fileselect', [numFiles, label]);
+});
+
+$(document).ready( function() {
+	$('.btn-file :file').on('fileselect', function(event, numFiles, label) {
+
+		var input = $(this).parents('.input-group').find(':text'),
+			log = numFiles > 1 ? numFiles + ' files selected' : label;
+
+		if( input.length ) {
+			input.val(log);
+		} else {
+			if( log ) alert(log);
+		}
+	});
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/jquery-ui.min.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/jquery-ui.min.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/jquery-ui.min.js	(revision 16045)
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.10.4 - 2014-01-17
+* http://jqueryui.com
+* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js, jquery.ui.menu.js, jquery.ui.progressbar.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.slider.js, jquery.ui.sortable.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,a="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:a?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType;return{element:i,isWindow:s,isDocument:n,offset:i.offset()||{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:s?i.width():i.outerWidth(),height:s?i.height():i.outerHeight()}}},t.fn.position=function(e){if(!e||!e.of)return f.apply(this,arguments);e=t.extend({},e);var a,p,g,m,v,_,b=t(e.of),y=t.position.getWithinInfo(e.within),k=t.position.getScrollInfo(y),w=(e.collision||"flip").split(" "),D={};return _=n(b),b[0].preventDefault&&(e.at="left top"),p=_.width,g=_.height,m=_.offset,v=t.extend({},m),t.each(["my","at"],function(){var t,i,s=(e[this]||"").split(" ");1===s.length&&(s=h.test(s[0])?s.concat(["center"]):c.test(s[0])?["center"].concat(s):["center","center"]),s[0]=h.test(s[0])?s[0]:"center",s[1]=c.test(s[1])?s[1]:"center",t=u.exec(s[0]),i=u.exec(s[1]),D[this]=[t?t[0]:0,i?i[0]:0],e[this]=[d.exec(s[0])[0],d.exec(s[1])[0]]}),1===w.length&&(w[1]=w[0]),"right"===e.at[0]?v.left+=p:"center"===e.at[0]&&(v.left+=p/2),"bottom"===e.at[1]?v.top+=g:"center"===e.at[1]&&(v.top+=g/2),a=i(D.at,p,g),v.left+=a[0],v.top+=a[1],this.each(function(){var n,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=s(this,"marginLeft"),_=s(this,"marginTop"),x=u+f+s(this,"marginRight")+k.width,C=d+_+s(this,"marginBottom")+k.height,M=t.extend({},v),T=i(D.my,c.outerWidth(),c.outerHeight());"right"===e.my[0]?M.left-=u:"center"===e.my[0]&&(M.left-=u/2),"bottom"===e.my[1]?M.top-=d:"center"===e.my[1]&&(M.top-=d/2),M.left+=T[0],M.top+=T[1],t.support.offsetFractions||(M.left=l(M.left),M.top=l(M.top)),n={marginLeft:f,marginTop:_},t.each(["left","top"],function(i,s){t.ui.position[w[i]]&&t.ui.position[w[i]][s](M,{targetWidth:p,targetHeight:g,elemWidth:u,elemHeight:d,collisionPosition:n,collisionWidth:x,collisionHeight:C,offset:[a[0]+T[0],a[1]+T[1]],my:e.my,at:e.at,within:y,elem:c})}),e.using&&(h=function(t){var i=m.left-M.left,s=i+p-u,n=m.top-M.top,a=n+g-d,l={target:{element:b,left:m.left,top:m.top,width:p,height:g},element:{element:c,left:M.left,top:M.top,width:u,height:d},horizontal:0>s?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){var t=0,i={},a={};i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="hide",a.height=a.paddingTop=a.paddingBottom=a.borderTopWidth=a.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.10.4",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},_create:function(){var t=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),t.collapsible||t.active!==!1&&null!=t.active||(t.active=0),this._processPanels(),0>t.active&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():e(),content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("<span>").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),"content"!==this.options.heightStyle&&e.css("height","")},_setOption:function(e,t){return"active"===e?(this._activate(t),undefined):("event"===e&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),"collapsible"!==e||t||this.options.active!==!1||this._activate(0),"icons"===e&&(this._destroyIcons(),t&&this._createIcons()),"disabled"===e&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t),undefined)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var i=e.ui.keyCode,a=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case i.RIGHT:case i.DOWN:n=this.headers[(s+1)%a];break;case i.LEFT:case i.UP:n=this.headers[(s-1+a)%a];break;case i.SPACE:case i.ENTER:this._eventHandler(t);break;case i.HOME:n=this.headers[0];break;case i.END:n=this.headers[a-1]}n&&(e(t.target).attr("tabIndex",-1),e(n).attr("tabIndex",0),n.focus(),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:function(){var t=this.options;this._processPanels(),t.active===!1&&t.collapsible===!0||!this.headers.length?(t.active=!1,this.active=e()):t.active===!1?this._activate(0):this.active.length&&!e.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=e()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide()},_refresh:function(){var i,a=this.options,s=a.heightStyle,n=this.element.parent(),r=this.accordionId="ui-accordion-"+(this.element.attr("id")||++t);this.active=this._findActive(a.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(t){var i=e(this),a=i.attr("id"),s=i.next(),n=s.attr("id");a||(a=r+"-header-"+t,i.attr("id",a)),n||(n=r+"-panel-"+t,s.attr("id",n)),i.attr("aria-controls",n),s.attr("aria-labelledby",a)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(a.event),"fill"===s?(i=n.height(),this.element.siblings(":visible").each(function(){var t=e(this),a=t.css("position");"absolute"!==a&&"fixed"!==a&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===s&&(i=0,this.headers.next().each(function(){i=Math.max(i,e(this).css("height","").height())}).height(i))},_activate:function(t){var i=this._findActive(t)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):e()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var i=this.options,a=this.active,s=e(t.currentTarget),n=s[0]===a[0],r=n&&i.collapsible,o=r?e():s.next(),h=a.next(),d={oldHeader:a,oldPanel:h,newHeader:r?e():s,newPanel:o};t.preventDefault(),n&&!i.collapsible||this._trigger("beforeActivate",t,d)===!1||(i.active=r?!1:this.headers.index(s),this.active=n?e():s,this._toggle(d),a.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&a.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),n||(s.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),s.next().addClass("ui-accordion-content-active")))},_toggle:function(t){var i=t.newPanel,a=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=a,this.options.animate?this._animate(i,a,t):(a.hide(),i.show(),this._toggleComplete(t)),a.attr({"aria-hidden":"true"}),a.prev().attr("aria-selected","false"),i.length&&a.length?a.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(e,t,s){var n,r,o,h=this,d=0,c=e.length&&(!t.length||e.index()<t.index()),l=this.options.animate||{},u=c&&l.down||l,v=function(){h._toggleComplete(s)};return"number"==typeof u&&(o=u),"string"==typeof u&&(r=u),r=r||u.easing||l.easing,o=o||u.duration||l.duration,t.length?e.length?(n=e.show().outerHeight(),t.animate(i,{duration:o,easing:r,step:function(e,t){t.now=Math.round(e)}}),e.hide().animate(a,{duration:o,easing:r,complete:v,step:function(e,i){i.now=Math.round(e),"height"!==i.prop?d+=i.now:"content"!==h.options.heightStyle&&(i.now=Math.round(n-t.outerHeight()-d),d=0)}}),undefined):t.animate(i,o,r,v):e.animate(a,o,r,v)},_toggleComplete:function(e){var t=e.oldPanel;t.removeClass("ui-accordion-content-active").prev().removeClass("ui-corner-top").addClass("ui-corner-all"),t.length&&(t.parent()[0].className=t.parent()[0].className),this._trigger("activate",null,e)}})})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("<ul>").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("<span>",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length<this.options.minLength?this.close(t):this._trigger("search",t)!==!1?this._search(e):undefined},_search:function(e){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:e},this._response())},_response:function(){var t=++this.requestIndex;return e.proxy(function(e){t===this.requestIndex&&this.__response(e),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},this)},__response:function(e){e&&(e=this._normalize(e)),this._trigger("response",null,{content:e}),!this.options.disabled&&e&&e.length&&!this.cancelSearch?(this._suggest(e),this._trigger("open")):this._close()},close:function(e){this.cancelSearch=!0,this._close(e)},_close:function(e){this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",e))},_change:function(e){this.previous!==this._value()&&this._trigger("change",e,{item:this.selectedItem})},_normalize:function(t){return t.length&&t[0].label&&t[0].value?t:e.map(t,function(t){return"string"==typeof t?{label:t,value:t}:e.extend({label:t.label||t.value,value:t.value||t.label},t)})},_suggest:function(t){var i=this.menu.element.empty();this._renderMenu(i,t),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(e.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next()},_resizeMenu:function(){var e=this.menu.element;e.outerWidth(Math.max(e.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(t,i){var s=this;e.each(i,function(e,i){s._renderItemData(t,i)})},_renderItemData:function(e,t){return this._renderItem(e,t).data("ui-autocomplete-item",t)},_renderItem:function(t,i){return e("<li>").append(e("<a>").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(e){var t,i="ui-button ui-widget ui-state-default ui-corner-all",n="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",s=function(){var t=e(this);setTimeout(function(){t.find(":ui-button").button("refresh")},1)},a=function(t){var i=t.name,n=t.form,s=e([]);return i&&(i=i.replace(/'/g,"\\'"),s=n?e(n).find("[name='"+i+"']"):e("[name='"+i+"']",t.ownerDocument).filter(function(){return!this.form})),s};e.widget("ui.button",{version:"1.10.4",defaultElement:"<button>",options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset"+this.eventNamespace).bind("reset"+this.eventNamespace,s),"boolean"!=typeof this.options.disabled?this.options.disabled=!!this.element.prop("disabled"):this.element.prop("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var n=this,o=this.options,r="checkbox"===this.type||"radio"===this.type,h=r?"":"ui-state-active";null===o.label&&(o.label="input"===this.type?this.buttonElement.val():this.buttonElement.html()),this._hoverable(this.buttonElement),this.buttonElement.addClass(i).attr("role","button").bind("mouseenter"+this.eventNamespace,function(){o.disabled||this===t&&e(this).addClass("ui-state-active")}).bind("mouseleave"+this.eventNamespace,function(){o.disabled||e(this).removeClass(h)}).bind("click"+this.eventNamespace,function(e){o.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}),this._on({focus:function(){this.buttonElement.addClass("ui-state-focus")},blur:function(){this.buttonElement.removeClass("ui-state-focus")}}),r&&this.element.bind("change"+this.eventNamespace,function(){n.refresh()}),"checkbox"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){return o.disabled?!1:undefined}):"radio"===this.type?this.buttonElement.bind("click"+this.eventNamespace,function(){if(o.disabled)return!1;e(this).addClass("ui-state-active"),n.buttonElement.attr("aria-pressed","true");var t=n.element[0];a(t).not(t).map(function(){return e(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown"+this.eventNamespace,function(){return o.disabled?!1:(e(this).addClass("ui-state-active"),t=this,n.document.one("mouseup",function(){t=null}),undefined)}).bind("mouseup"+this.eventNamespace,function(){return o.disabled?!1:(e(this).removeClass("ui-state-active"),undefined)}).bind("keydown"+this.eventNamespace,function(t){return o.disabled?!1:((t.keyCode===e.ui.keyCode.SPACE||t.keyCode===e.ui.keyCode.ENTER)&&e(this).addClass("ui-state-active"),undefined)}).bind("keyup"+this.eventNamespace+" blur"+this.eventNamespace,function(){e(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(t){t.keyCode===e.ui.keyCode.SPACE&&e(this).click()})),this._setOption("disabled",o.disabled),this._resetButton()},_determineButtonType:function(){var e,t,i;this.type=this.element.is("[type=checkbox]")?"checkbox":this.element.is("[type=radio]")?"radio":this.element.is("input")?"input":"button","checkbox"===this.type||"radio"===this.type?(e=this.element.parents().last(),t="label[for='"+this.element.attr("id")+"']",this.buttonElement=e.find(t),this.buttonElement.length||(e=e.length?e.siblings():this.element.siblings(),this.buttonElement=e.filter(t),this.buttonElement.length||(this.buttonElement=e.find(t))),this.element.addClass("ui-helper-hidden-accessible"),i=this.element.is(":checked"),i&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.prop("aria-pressed",i)):this.buttonElement=this.element},widget:function(){return this.buttonElement},_destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(i+" ui-state-active "+n).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title")},_setOption:function(e,t){return this._super(e,t),"disabled"===e?(this.element.prop("disabled",!!t),t&&this.buttonElement.removeClass("ui-state-focus"),undefined):(this._resetButton(),undefined)},refresh:function(){var t=this.element.is("input, button")?this.element.is(":disabled"):this.element.hasClass("ui-button-disabled");t!==this.options.disabled&&this._setOption("disabled",t),"radio"===this.type?a(this.element[0]).each(function(){e(this).is(":checked")?e(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):e(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):"checkbox"===this.type&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if("input"===this.type)return this.options.label&&this.element.val(this.options.label),undefined;var t=this.buttonElement.removeClass(n),i=e("<span></span>",this.document[0]).addClass("ui-button-text").html(this.options.label).appendTo(t.empty()).text(),s=this.options.icons,a=s.primary&&s.secondary,o=[];s.primary||s.secondary?(this.options.text&&o.push("ui-button-text-icon"+(a?"s":s.primary?"-primary":"-secondary")),s.primary&&t.prepend("<span class='ui-button-icon-primary ui-icon "+s.primary+"'></span>"),s.secondary&&t.append("<span class='ui-button-icon-secondary ui-icon "+s.secondary+"'></span>"),this.options.text||(o.push(a?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||t.attr("title",e.trim(i)))):o.push("ui-button-text-only"),t.addClass(o.join(" "))}}),e.widget("ui.buttonset",{version:"1.10.4",options:{items:"button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(e,t){"disabled"===e&&this.buttons.button("option",e,t),this._super(e,t)},refresh:function(){var t="rtl"===this.element.css("direction");this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(t?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(t?"ui-corner-left":"ui-corner-right").end().end()},_destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return e(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy")}})})(jQuery);(function(e,t){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.dpDiv=a(e("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){e.datepicker._isDisabledDatepicker(n.inline?t.parent()[0]:n.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))})}function s(t,i){e.extend(t,i);for(var a in i)null==i[a]&&(t[a]=i[a]);return t}e.extend(e.ui,{datepicker:{version:"1.10.4"}});var n,r="datepicker";e.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return s(this._defaults,e||{}),this},_attachDatepicker:function(t,i){var a,s,n;a=t.nodeName.toLowerCase(),s="div"===a||"span"===a,t.id||(this.uuid+=1,t.id="dp"+this.uuid),n=this._newInst(e(t),s),n.settings=e.extend({},i||{}),"input"===a?this._connectDatepicker(t,n):s&&this._inlineDatepicker(t,n)},_newInst:function(t,i){var s=t[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:s,input:t,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?a(e("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(t,i){var a=e(t);i.append=e([]),i.trigger=e([]),a.hasClass(this.markerClassName)||(this._attachments(a,i),a.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),e.data(t,r,i),i.settings.disabled&&this._disableDatepicker(t))},_attachments:function(t,i){var a,s,n,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=e("<span class='"+this._appendClass+"'>"+r+"</span>"),t[o?"before":"after"](i.append)),t.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),a=this._get(i,"showOn"),("focus"===a||"both"===a)&&t.focus(this._showDatepicker),("button"===a||"both"===a)&&(s=this._get(i,"buttonText"),n=this._get(i,"buttonImage"),i.trigger=e(this._get(i,"buttonImageOnly")?e("<img/>").addClass(this._triggerClass).attr({src:n,alt:s,title:s}):e("<button type='button'></button>").addClass(this._triggerClass).html(n?e("<img/>").attr({src:n,alt:s,title:s}):s)),t[o?"before":"after"](i.trigger),i.trigger.click(function(){return e.datepicker._datepickerShowing&&e.datepicker._lastInput===t[0]?e.datepicker._hideDatepicker():e.datepicker._datepickerShowing&&e.datepicker._lastInput!==t[0]?(e.datepicker._hideDatepicker(),e.datepicker._showDatepicker(t[0])):e.datepicker._showDatepicker(t[0]),!1}))},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t,i,a,s,n=new Date(2009,11,20),r=this._get(e,"dateFormat");r.match(/[DM]/)&&(t=function(e){for(i=0,a=0,s=0;e.length>s;s++)e[s].length>i&&(i=e[s].length,a=s);return a},n.setMonth(t(this._get(e,r.match(/MM/)?"monthNames":"monthNamesShort"))),n.setDate(t(this._get(e,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-n.getDay())),e.input.attr("size",this._formatDate(e,n).length)}},_inlineDatepicker:function(t,i){var a=e(t);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(i.dpDiv),e.data(t,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(t),i.dpDiv.css("display","block"))},_dialogDatepicker:function(t,i,a,n,o){var u,c,h,l,d,p=this._dialogInst;return p||(this.uuid+=1,u="dp"+this.uuid,this._dialogInput=e("<input type='text' id='"+u+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.keydown(this._doKeyDown),e("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},e.data(this._dialogInput[0],r,p)),s(p.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(c=document.documentElement.clientWidth,h=document.documentElement.clientHeight,l=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[c/2-100+l,h/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),e.blockUI&&e.blockUI(this.dpDiv),e.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(t){var i,a=e(t),s=e.data(t,r);a.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),e.removeData(t,r),"input"===i?(s.append.remove(),s.trigger.remove(),a.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&a.removeClass(this.markerClassName).empty())},_enableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;this._disabledInputs.length>t;t++)if(this._disabledInputs[t]===e)return!0;return!1},_getInst:function(t){try{return e.data(t,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,a,n){var r,o,u,c,h=this._getInst(i);return 2===arguments.length&&"string"==typeof a?"defaults"===a?e.extend({},e.datepicker._defaults):h?"all"===a?e.extend({},h.settings):this._get(h,a):null:(r=a||{},"string"==typeof a&&(r={},r[a]=n),h&&(this._curInst===h&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),u=this._getMinMaxDate(h,"min"),c=this._getMinMaxDate(h,"max"),s(h.settings,r),null!==u&&r.dateFormat!==t&&r.minDate===t&&(h.settings.minDate=this._formatDate(h,u)),null!==c&&r.dateFormat!==t&&r.maxDate===t&&(h.settings.maxDate=this._formatDate(h,c)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(e(i),h),this._autoSize(h),this._setDate(h,o),this._updateAlternate(h),this._updateDatepicker(h)),t)},_changeDatepicker:function(e,t,i){this._optionDatepicker(e,t,i)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var i=this._getInst(e);i&&(this._setDate(i,t),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(e,t){var i=this._getInst(e);return i&&!i.inline&&this._setDateFromField(i,t),i?this._getDate(i):null},_doKeyDown:function(t){var i,a,s,n=e.datepicker._getInst(t.target),r=!0,o=n.dpDiv.is(".ui-datepicker-rtl");if(n._keyEvent=!0,e.datepicker._datepickerShowing)switch(t.keyCode){case 9:e.datepicker._hideDatepicker(),r=!1;break;case 13:return s=e("td."+e.datepicker._dayOverClass+":not(."+e.datepicker._currentClass+")",n.dpDiv),s[0]&&e.datepicker._selectDay(t.target,n.selectedMonth,n.selectedYear,s[0]),i=e.datepicker._get(n,"onSelect"),i?(a=e.datepicker._formatDate(n),i.apply(n.input?n.input[0]:null,[a,n])):e.datepicker._hideDatepicker(),!1;case 27:e.datepicker._hideDatepicker();break;case 33:e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 34:e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 35:(t.ctrlKey||t.metaKey)&&e.datepicker._clearDate(t.target),r=t.ctrlKey||t.metaKey;break;case 36:(t.ctrlKey||t.metaKey)&&e.datepicker._gotoToday(t.target),r=t.ctrlKey||t.metaKey;break;case 37:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?1:-1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 38:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,-7,"D"),r=t.ctrlKey||t.metaKey;break;case 39:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?-1:1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 40:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,7,"D"),r=t.ctrlKey||t.metaKey;break;default:r=!1}else 36===t.keyCode&&t.ctrlKey?e.datepicker._showDatepicker(this):r=!1;r&&(t.preventDefault(),t.stopPropagation())},_doKeyPress:function(i){var a,s,n=e.datepicker._getInst(i.target);return e.datepicker._get(n,"constrainInput")?(a=e.datepicker._possibleChars(e.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">s||!a||a.indexOf(s)>-1):t},_doKeyUp:function(t){var i,a=e.datepicker._getInst(t.target);if(a.input.val()!==a.lastVal)try{i=e.datepicker.parseDate(e.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,e.datepicker._getFormatConfig(a)),i&&(e.datepicker._setDateFromField(a),e.datepicker._updateAlternate(a),e.datepicker._updateDatepicker(a))}catch(s){}return!0},_showDatepicker:function(t){if(t=t.target||t,"input"!==t.nodeName.toLowerCase()&&(t=e("input",t.parentNode)[0]),!e.datepicker._isDisabledDatepicker(t)&&e.datepicker._lastInput!==t){var i,a,n,r,o,u,c;i=e.datepicker._getInst(t),e.datepicker._curInst&&e.datepicker._curInst!==i&&(e.datepicker._curInst.dpDiv.stop(!0,!0),i&&e.datepicker._datepickerShowing&&e.datepicker._hideDatepicker(e.datepicker._curInst.input[0])),a=e.datepicker._get(i,"beforeShow"),n=a?a.apply(t,[t,i]):{},n!==!1&&(s(i.settings,n),i.lastVal=null,e.datepicker._lastInput=t,e.datepicker._setDateFromField(i),e.datepicker._inDialog&&(t.value=""),e.datepicker._pos||(e.datepicker._pos=e.datepicker._findPos(t),e.datepicker._pos[1]+=t.offsetHeight),r=!1,e(t).parents().each(function(){return r|="fixed"===e(this).css("position"),!r}),o={left:e.datepicker._pos[0],top:e.datepicker._pos[1]},e.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),e.datepicker._updateDatepicker(i),o=e.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:e.datepicker._inDialog&&e.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(u=e.datepicker._get(i,"showAnim"),c=e.datepicker._get(i,"duration"),i.dpDiv.zIndex(e(t).zIndex()+1),e.datepicker._datepickerShowing=!0,e.effects&&e.effects.effect[u]?i.dpDiv.show(u,e.datepicker._get(i,"showOptions"),c):i.dpDiv[u||"show"](u?c:null),e.datepicker._shouldFocusInput(i)&&i.input.focus(),e.datepicker._curInst=i))}},_updateDatepicker:function(t){this.maxRows=4,n=t,t.dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t),t.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,a=this._getNumberOfMonths(t),s=a[1],r=17;t.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),s>1&&t.dpDiv.addClass("ui-datepicker-multi-"+s).css("width",r*s+"em"),t.dpDiv[(1!==a[0]||1!==a[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),t.dpDiv[(this._get(t,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),t===e.datepicker._curInst&&e.datepicker._datepickerShowing&&e.datepicker._shouldFocusInput(t)&&t.input.focus(),t.yearshtml&&(i=t.yearshtml,setTimeout(function(){i===t.yearshtml&&t.yearshtml&&t.dpDiv.find("select.ui-datepicker-year:first").replaceWith(t.yearshtml),i=t.yearshtml=null},0))},_shouldFocusInput:function(e){return e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&!e.input.is(":focus")},_checkOffset:function(t,i,a){var s=t.dpDiv.outerWidth(),n=t.dpDiv.outerHeight(),r=t.input?t.input.outerWidth():0,o=t.input?t.input.outerHeight():0,u=document.documentElement.clientWidth+(a?0:e(document).scrollLeft()),c=document.documentElement.clientHeight+(a?0:e(document).scrollTop());return i.left-=this._get(t,"isRTL")?s-r:0,i.left-=a&&i.left===t.input.offset().left?e(document).scrollLeft():0,i.top-=a&&i.top===t.input.offset().top+o?e(document).scrollTop():0,i.left-=Math.min(i.left,i.left+s>u&&u>s?Math.abs(i.left+s-u):0),i.top-=Math.min(i.top,i.top+n>c&&c>n?Math.abs(n+o):0),i},_findPos:function(t){for(var i,a=this._getInst(t),s=this._get(a,"isRTL");t&&("hidden"===t.type||1!==t.nodeType||e.expr.filters.hidden(t));)t=t[s?"previousSibling":"nextSibling"];return i=e(t).offset(),[i.left,i.top]},_hideDatepicker:function(t){var i,a,s,n,o=this._curInst;!o||t&&o!==e.data(t,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),a=this._get(o,"duration"),s=function(){e.datepicker._tidyDialog(o)},e.effects&&(e.effects.effect[i]||e.effects[i])?o.dpDiv.hide(i,e.datepicker._get(o,"showOptions"),a,s):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?a:null,s),i||s(),this._datepickerShowing=!1,n=this._get(o,"onClose"),n&&n.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),e.blockUI&&(e.unblockUI(),e("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(t){if(e.datepicker._curInst){var i=e(t.target),a=e.datepicker._getInst(i[0]);(i[0].id!==e.datepicker._mainDivId&&0===i.parents("#"+e.datepicker._mainDivId).length&&!i.hasClass(e.datepicker.markerClassName)&&!i.closest("."+e.datepicker._triggerClass).length&&e.datepicker._datepickerShowing&&(!e.datepicker._inDialog||!e.blockUI)||i.hasClass(e.datepicker.markerClassName)&&e.datepicker._curInst!==a)&&e.datepicker._hideDatepicker()}},_adjustDate:function(t,i,a){var s=e(t),n=this._getInst(s[0]);this._isDisabledDatepicker(s[0])||(this._adjustInstDate(n,i+("M"===a?this._get(n,"showCurrentAtPos"):0),a),this._updateDatepicker(n))},_gotoToday:function(t){var i,a=e(t),s=this._getInst(a[0]);this._get(s,"gotoCurrent")&&s.currentDay?(s.selectedDay=s.currentDay,s.drawMonth=s.selectedMonth=s.currentMonth,s.drawYear=s.selectedYear=s.currentYear):(i=new Date,s.selectedDay=i.getDate(),s.drawMonth=s.selectedMonth=i.getMonth(),s.drawYear=s.selectedYear=i.getFullYear()),this._notifyChange(s),this._adjustDate(a)},_selectMonthYear:function(t,i,a){var s=e(t),n=this._getInst(s[0]);n["selected"+("M"===a?"Month":"Year")]=n["draw"+("M"===a?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(n),this._adjustDate(s)},_selectDay:function(t,i,a,s){var n,r=e(t);e(s).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(n=this._getInst(r[0]),n.selectedDay=n.currentDay=e("a",s).html(),n.selectedMonth=n.currentMonth=i,n.selectedYear=n.currentYear=a,this._selectDate(t,this._formatDate(n,n.currentDay,n.currentMonth,n.currentYear)))},_clearDate:function(t){var i=e(t);this._selectDate(i,"")},_selectDate:function(t,i){var a,s=e(t),n=this._getInst(s[0]);i=null!=i?i:this._formatDate(n),n.input&&n.input.val(i),this._updateAlternate(n),a=this._get(n,"onSelect"),a?a.apply(n.input?n.input[0]:null,[i,n]):n.input&&n.input.trigger("change"),n.inline?this._updateDatepicker(n):(this._hideDatepicker(),this._lastInput=n.input[0],"object"!=typeof n.input[0]&&n.input.focus(),this._lastInput=null)},_updateAlternate:function(t){var i,a,s,n=this._get(t,"altField");n&&(i=this._get(t,"altFormat")||this._get(t,"dateFormat"),a=this._getDate(t),s=this.formatDate(i,a,this._getFormatConfig(t)),e(n).each(function(){e(this).val(s)}))},noWeekends:function(e){var t=e.getDay();return[t>0&&6>t,""]},iso8601Week:function(e){var t,i=new Date(e.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),t=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((t-i)/864e5)/7)+1},parseDate:function(i,a,s){if(null==i||null==a)throw"Invalid arguments";if(a="object"==typeof a?""+a:a+"",""===a)return null;var n,r,o,u,c=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,l="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),d=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,p=(s?s.dayNames:null)||this._defaults.dayNames,g=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,m=(s?s.monthNames:null)||this._defaults.monthNames,f=-1,_=-1,v=-1,k=-1,y=!1,b=function(e){var t=i.length>n+1&&i.charAt(n+1)===e;return t&&n++,t},D=function(e){var t=b(e),i="@"===e?14:"!"===e?20:"y"===e&&t?4:"o"===e?3:2,s=RegExp("^\\d{1,"+i+"}"),n=a.substring(c).match(s);if(!n)throw"Missing number at position "+c;return c+=n[0].length,parseInt(n[0],10)},w=function(i,s,n){var r=-1,o=e.map(b(i)?n:s,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)});if(e.each(o,function(e,i){var s=i[1];return a.substr(c,s.length).toLowerCase()===s.toLowerCase()?(r=i[0],c+=s.length,!1):t}),-1!==r)return r+1;throw"Unknown name at position "+c},M=function(){if(a.charAt(c)!==i.charAt(n))throw"Unexpected literal at position "+c;c++};for(n=0;i.length>n;n++)if(y)"'"!==i.charAt(n)||b("'")?M():y=!1;else switch(i.charAt(n)){case"d":v=D("d");break;case"D":w("D",d,p);break;case"o":k=D("o");break;case"m":_=D("m");break;case"M":_=w("M",g,m);break;case"y":f=D("y");break;case"@":u=new Date(D("@")),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"!":u=new Date((D("!")-this._ticksTo1970)/1e4),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"'":b("'")?M():y=!0;break;default:M()}if(a.length>c&&(o=a.substr(c),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===f?f=(new Date).getFullYear():100>f&&(f+=(new Date).getFullYear()-(new Date).getFullYear()%100+(l>=f?0:-100)),k>-1)for(_=1,v=k;;){if(r=this._getDaysInMonth(f,_-1),r>=v)break;_++,v-=r}if(u=this._daylightSavingAdjust(new Date(f,_-1,v)),u.getFullYear()!==f||u.getMonth()+1!==_||u.getDate()!==v)throw"Invalid date";return u},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(e,t,i){if(!t)return"";var a,s=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,n=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,u=function(t){var i=e.length>a+1&&e.charAt(a+1)===t;return i&&a++,i},c=function(e,t,i){var a=""+t;if(u(e))for(;i>a.length;)a="0"+a;return a},h=function(e,t,i,a){return u(e)?a[t]:i[t]},l="",d=!1;if(t)for(a=0;e.length>a;a++)if(d)"'"!==e.charAt(a)||u("'")?l+=e.charAt(a):d=!1;else switch(e.charAt(a)){case"d":l+=c("d",t.getDate(),2);break;case"D":l+=h("D",t.getDay(),s,n);break;case"o":l+=c("o",Math.round((new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()-new Date(t.getFullYear(),0,0).getTime())/864e5),3);break;case"m":l+=c("m",t.getMonth()+1,2);break;case"M":l+=h("M",t.getMonth(),r,o);break;case"y":l+=u("y")?t.getFullYear():(10>t.getYear()%100?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=1e4*t.getTime()+this._ticksTo1970;break;case"'":u("'")?l+="'":d=!0;break;default:l+=e.charAt(a)}return l},_possibleChars:function(e){var t,i="",a=!1,s=function(i){var a=e.length>t+1&&e.charAt(t+1)===i;return a&&t++,a};for(t=0;e.length>t;t++)if(a)"'"!==e.charAt(t)||s("'")?i+=e.charAt(t):a=!1;else switch(e.charAt(t)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":s("'")?i+="'":a=!0;break;default:i+=e.charAt(t)}return i},_get:function(e,i){return e.settings[i]!==t?e.settings[i]:this._defaults[i]},_setDateFromField:function(e,t){if(e.input.val()!==e.lastVal){var i=this._get(e,"dateFormat"),a=e.lastVal=e.input?e.input.val():null,s=this._getDefaultDate(e),n=s,r=this._getFormatConfig(e);try{n=this.parseDate(i,a,r)||s}catch(o){a=t?"":a}e.selectedDay=n.getDate(),e.drawMonth=e.selectedMonth=n.getMonth(),e.drawYear=e.selectedYear=n.getFullYear(),e.currentDay=a?n.getDate():0,e.currentMonth=a?n.getMonth():0,e.currentYear=a?n.getFullYear():0,this._adjustInstDate(e)}},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(t,i,a){var s=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},n=function(i){try{return e.datepicker.parseDate(e.datepicker._get(t,"dateFormat"),i,e.datepicker._getFormatConfig(t))}catch(a){}for(var s=(i.toLowerCase().match(/^c/)?e.datepicker._getDate(t):null)||new Date,n=s.getFullYear(),r=s.getMonth(),o=s.getDate(),u=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,c=u.exec(i);c;){switch(c[2]||"d"){case"d":case"D":o+=parseInt(c[1],10);break;case"w":case"W":o+=7*parseInt(c[1],10);break;case"m":case"M":r+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r));break;case"y":case"Y":n+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r))}c=u.exec(i)}return new Date(n,r,o)},r=null==i||""===i?a:"string"==typeof i?n(i):"number"==typeof i?isNaN(i)?a:s(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?a:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,i){var a=!t,s=e.selectedMonth,n=e.selectedYear,r=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=r.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=r.getMonth(),e.drawYear=e.selectedYear=e.currentYear=r.getFullYear(),s===e.selectedMonth&&n===e.selectedYear||i||this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(a?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&""===e.input.val()?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(t){var i=this._get(t,"stepMonths"),a="#"+t.id.replace(/\\\\/g,"\\");t.dpDiv.find("[data-handler]").map(function(){var t={prev:function(){e.datepicker._adjustDate(a,-i,"M")},next:function(){e.datepicker._adjustDate(a,+i,"M")},hide:function(){e.datepicker._hideDatepicker()},today:function(){e.datepicker._gotoToday(a)},selectDay:function(){return e.datepicker._selectDay(a,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return e.datepicker._selectMonthYear(a,this,"M"),!1},selectYear:function(){return e.datepicker._selectMonthYear(a,this,"Y"),!1}};e(this).bind(this.getAttribute("data-event"),t[this.getAttribute("data-handler")])})},_generateHTML:function(e){var t,i,a,s,n,r,o,u,c,h,l,d,p,g,m,f,_,v,k,y,b,D,w,M,C,x,I,N,T,A,E,S,Y,F,P,O,j,K,R,H=new Date,W=this._daylightSavingAdjust(new Date(H.getFullYear(),H.getMonth(),H.getDate())),L=this._get(e,"isRTL"),U=this._get(e,"showButtonPanel"),B=this._get(e,"hideIfNoPrevNext"),z=this._get(e,"navigationAsDateFormat"),q=this._getNumberOfMonths(e),G=this._get(e,"showCurrentAtPos"),J=this._get(e,"stepMonths"),Q=1!==q[0]||1!==q[1],V=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),$=this._getMinMaxDate(e,"min"),X=this._getMinMaxDate(e,"max"),Z=e.drawMonth-G,et=e.drawYear;if(0>Z&&(Z+=12,et--),X)for(t=this._daylightSavingAdjust(new Date(X.getFullYear(),X.getMonth()-q[0]*q[1]+1,X.getDate())),t=$&&$>t?$:t;this._daylightSavingAdjust(new Date(et,Z,1))>t;)Z--,0>Z&&(Z=11,et--);for(e.drawMonth=Z,e.drawYear=et,i=this._get(e,"prevText"),i=z?this.formatDate(i,this._daylightSavingAdjust(new Date(et,Z-J,1)),this._getFormatConfig(e)):i,a=this._canAdjustMonth(e,-1,et,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>":B?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>",s=this._get(e,"nextText"),s=z?this.formatDate(s,this._daylightSavingAdjust(new Date(et,Z+J,1)),this._getFormatConfig(e)):s,n=this._canAdjustMonth(e,1,et,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>":B?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>",r=this._get(e,"currentText"),o=this._get(e,"gotoCurrent")&&e.currentDay?V:W,r=z?this.formatDate(r,o,this._getFormatConfig(e)):r,u=e.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(e,"closeText")+"</button>",c=U?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(L?u:"")+(this._isInRange(e,o)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+r+"</button>":"")+(L?"":u)+"</div>":"",h=parseInt(this._get(e,"firstDay"),10),h=isNaN(h)?0:h,l=this._get(e,"showWeek"),d=this._get(e,"dayNames"),p=this._get(e,"dayNamesMin"),g=this._get(e,"monthNames"),m=this._get(e,"monthNamesShort"),f=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),v=this._get(e,"selectOtherMonths"),k=this._getDefaultDate(e),y="",D=0;q[0]>D;D++){for(w="",this.maxRows=4,M=0;q[1]>M;M++){if(C=this._daylightSavingAdjust(new Date(et,Z,e.selectedDay)),x=" ui-corner-all",I="",Q){if(I+="<div class='ui-datepicker-group",q[1]>1)switch(M){case 0:I+=" ui-datepicker-group-first",x=" ui-corner-"+(L?"right":"left");break;case q[1]-1:I+=" ui-datepicker-group-last",x=" ui-corner-"+(L?"left":"right");break;default:I+=" ui-datepicker-group-middle",x=""}I+="'>"}for(I+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+x+"'>"+(/all|left/.test(x)&&0===D?L?n:a:"")+(/all|right/.test(x)&&0===D?L?a:n:"")+this._generateMonthYearHeader(e,Z,et,$,X,D>0||M>0,g,m)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",N=l?"<th class='ui-datepicker-week-col'>"+this._get(e,"weekHeader")+"</th>":"",b=0;7>b;b++)T=(b+h)%7,N+="<th"+((b+h+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[T]+"'>"+p[T]+"</span></th>";for(I+=N+"</tr></thead><tbody>",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),E=(this._getFirstDayOfMonth(et,Z)-h+7)%7,S=Math.ceil((E+A)/7),Y=Q?this.maxRows>S?this.maxRows:S:S,this.maxRows=Y,F=this._daylightSavingAdjust(new Date(et,Z,1-E)),P=0;Y>P;P++){for(I+="<tr>",O=l?"<td class='ui-datepicker-week-col'>"+this._get(e,"calculateWeek")(F)+"</td>":"",b=0;7>b;b++)j=f?f.apply(e.input?e.input[0]:null,[F]):[!0,""],K=F.getMonth()!==Z,R=K&&!v||!j[0]||$&&$>F||X&&F>X,O+="<td class='"+((b+h+6)%7>=5?" ui-datepicker-week-end":"")+(K?" ui-datepicker-other-month":"")+(F.getTime()===C.getTime()&&Z===e.selectedMonth&&e._keyEvent||k.getTime()===F.getTime()&&k.getTime()===C.getTime()?" "+this._dayOverClass:"")+(R?" "+this._unselectableClass+" ui-state-disabled":"")+(K&&!_?"":" "+j[1]+(F.getTime()===V.getTime()?" "+this._currentClass:"")+(F.getTime()===W.getTime()?" ui-datepicker-today":""))+"'"+(K&&!_||!j[2]?"":" title='"+j[2].replace(/'/g,"&#39;")+"'")+(R?"":" data-handler='selectDay' data-event='click' data-month='"+F.getMonth()+"' data-year='"+F.getFullYear()+"'")+">"+(K&&!_?"&#xa0;":R?"<span class='ui-state-default'>"+F.getDate()+"</span>":"<a class='ui-state-default"+(F.getTime()===W.getTime()?" ui-state-highlight":"")+(F.getTime()===V.getTime()?" ui-state-active":"")+(K?" ui-priority-secondary":"")+"' href='#'>"+F.getDate()+"</a>")+"</td>",F.setDate(F.getDate()+1),F=this._daylightSavingAdjust(F);I+=O+"</tr>"}Z++,Z>11&&(Z=0,et++),I+="</tbody></table>"+(Q?"</div>"+(q[0]>0&&M===q[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),w+=I}y+=w}return y+=c,e._keyEvent=!1,y},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var u,c,h,l,d,p,g,m,f=this._get(e,"changeMonth"),_=this._get(e,"changeYear"),v=this._get(e,"showMonthAfterYear"),k="<div class='ui-datepicker-title'>",y="";if(n||!f)y+="<span class='ui-datepicker-month'>"+r[t]+"</span>";else{for(u=a&&a.getFullYear()===i,c=s&&s.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",h=0;12>h;h++)(!u||h>=a.getMonth())&&(!c||s.getMonth()>=h)&&(y+="<option value='"+h+"'"+(h===t?" selected='selected'":"")+">"+o[h]+"</option>");y+="</select>"}if(v||(k+=y+(!n&&f&&_?"":"&#xa0;")),!e.yearshtml)if(e.yearshtml="",n||!_)k+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(l=this._get(e,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?d+parseInt(e,10):parseInt(e,10);
+return isNaN(t)?d:t},g=p(l[0]),m=Math.max(g,p(l[1]||"")),g=a?Math.max(g,a.getFullYear()):g,m=s?Math.min(m,s.getFullYear()):m,e.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";m>=g;g++)e.yearshtml+="<option value='"+g+"'"+(g===i?" selected='selected'":"")+">"+g+"</option>";e.yearshtml+="</select>",k+=e.yearshtml,e.yearshtml=null}return k+=this._get(e,"yearSuffix"),v&&(k+=(!n&&f&&_?"":"&#xa0;")+y),k+="</div>"},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"===i?t:0),s=e.drawMonth+("M"===i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"===i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i,a,s=this._getMinMaxDate(e,"min"),n=this._getMinMaxDate(e,"max"),r=null,o=null,u=this._get(e,"yearRange");return u&&(i=u.split(":"),a=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=a),i[1].match(/[+\-].*/)&&(o+=a)),(!s||t.getTime()>=s.getTime())&&(!n||t.getTime()<=n.getTime())&&(!r||t.getFullYear()>=r)&&(!o||o>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new i,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.10.4"})(jQuery);(function(e){var t={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},i={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.10.4",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var i=e(this).css(t).offset().top;0>i&&e(this).css("top",t.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&e.fn.draggable&&this._makeDraggable(),this.options.resizable&&e.fn.resizable&&this._makeResizable(),this._isOpen=!1},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var t=this.options.appendTo;return t&&(t.jquery||t.nodeType)?e(t):this.document.find(t||"body").eq(0)},_destroy:function(){var e,t=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},disable:e.noop,enable:e.noop,close:function(t){var i,a=this;if(this._isOpen&&this._trigger("beforeClose",t)!==!1){if(this._isOpen=!1,this._destroyOverlay(),!this.opener.filter(":focusable").focus().length)try{i=this.document[0].activeElement,i&&"body"!==i.nodeName.toLowerCase()&&e(i).blur()}catch(s){}this._hide(this.uiDialog,this.options.hide,function(){a._trigger("close",t)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,t){var i=!!this.uiDialog.nextAll(":visible").insertBefore(this.uiDialog).length;return i&&!t&&this._trigger("focus",e),i},open:function(){var t=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),undefined):(this._isOpen=!0,this.opener=e(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this._show(this.uiDialog,this.options.show,function(){t._focusTabbable(),t._trigger("focus")}),this._trigger("open"),undefined)},_focusTabbable:function(){var e=this.element.find("[autofocus]");e.length||(e=this.element.find(":tabbable")),e.length||(e=this.uiDialogButtonPane.find(":tabbable")),e.length||(e=this.uiDialogTitlebarClose.filter(":tabbable")),e.length||(e=this.uiDialog),e.eq(0).focus()},_keepFocus:function(t){function i(){var t=this.document[0].activeElement,i=this.uiDialog[0]===t||e.contains(this.uiDialog[0],t);i||this._focusTabbable()}t.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=e("<div>").addClass("ui-dialog ui-widget ui-widget-content ui-corner-all ui-front "+this.options.dialogClass).hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._on(this.uiDialog,{keydown:function(t){if(this.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE)return t.preventDefault(),this.close(t),undefined;if(t.keyCode===e.ui.keyCode.TAB){var i=this.uiDialog.find(":tabbable"),a=i.filter(":first"),s=i.filter(":last");t.target!==s[0]&&t.target!==this.uiDialog[0]||t.shiftKey?t.target!==a[0]&&t.target!==this.uiDialog[0]||!t.shiftKey||(s.focus(1),t.preventDefault()):(a.focus(1),t.preventDefault())}},mousedown:function(e){this._moveToTop(e)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var t;this.uiDialogTitlebar=e("<div>").addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(this.uiDialog),this._on(this.uiDialogTitlebar,{mousedown:function(t){e(t.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.focus()}}),this.uiDialogTitlebarClose=e("<button type='button'></button>").button({label:this.options.closeText,icons:{primary:"ui-icon-closethick"},text:!1}).addClass("ui-dialog-titlebar-close").appendTo(this.uiDialogTitlebar),this._on(this.uiDialogTitlebarClose,{click:function(e){e.preventDefault(),this.close(e)}}),t=e("<span>").uniqueId().addClass("ui-dialog-title").prependTo(this.uiDialogTitlebar),this._title(t),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(e){this.options.title||e.html("&#160;"),e.text(this.options.title)},_createButtonPane:function(){this.uiDialogButtonPane=e("<div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),this.uiButtonSet=e("<div>").addClass("ui-dialog-buttonset").appendTo(this.uiDialogButtonPane),this._createButtons()},_createButtons:function(){var t=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),e.isEmptyObject(i)||e.isArray(i)&&!i.length?(this.uiDialog.removeClass("ui-dialog-buttons"),undefined):(e.each(i,function(i,a){var s,n;a=e.isFunction(a)?{click:a,text:i}:a,a=e.extend({type:"button"},a),s=a.click,a.click=function(){s.apply(t.element[0],arguments)},n={icons:a.icons,text:a.showText},delete a.icons,delete a.showText,e("<button></button>",a).button(n).appendTo(t.uiButtonSet)}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),undefined)},_makeDraggable:function(){function t(e){return{position:e.position,offset:e.offset}}var i=this,a=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(a,s){e(this).addClass("ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",a,t(s))},drag:function(e,a){i._trigger("drag",e,t(a))},stop:function(s,n){a.position=[n.position.left-i.document.scrollLeft(),n.position.top-i.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",s,t(n))}})},_makeResizable:function(){function t(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}var i=this,a=this.options,s=a.resizable,n=this.uiDialog.css("position"),r="string"==typeof s?s:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:a.maxWidth,maxHeight:a.maxHeight,minWidth:a.minWidth,minHeight:this._minHeight(),handles:r,start:function(a,s){e(this).addClass("ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",a,t(s))},resize:function(e,a){i._trigger("resize",e,t(a))},stop:function(s,n){a.height=e(this).height(),a.width=e(this).width(),e(this).removeClass("ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",s,t(n))}}).css("position",n)},_minHeight:function(){var e=this.options;return"auto"===e.height?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(){var e=this.uiDialog.is(":visible");e||this.uiDialog.show(),this.uiDialog.position(this.options.position),e||this.uiDialog.hide()},_setOptions:function(a){var s=this,n=!1,r={};e.each(a,function(e,a){s._setOption(e,a),e in t&&(n=!0),e in i&&(r[e]=a)}),n&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",r)},_setOption:function(e,t){var i,a,s=this.uiDialog;"dialogClass"===e&&s.removeClass(this.options.dialogClass).addClass(t),"disabled"!==e&&(this._super(e,t),"appendTo"===e&&this.uiDialog.appendTo(this._appendTo()),"buttons"===e&&this._createButtons(),"closeText"===e&&this.uiDialogTitlebarClose.button({label:""+t}),"draggable"===e&&(i=s.is(":data(ui-draggable)"),i&&!t&&s.draggable("destroy"),!i&&t&&this._makeDraggable()),"position"===e&&this._position(),"resizable"===e&&(a=s.is(":data(ui-resizable)"),a&&!t&&s.resizable("destroy"),a&&"string"==typeof t&&s.resizable("option","handles",t),a||t===!1||this._makeResizable()),"title"===e&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var e,t,i,a=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),a.minWidth>a.width&&(a.width=a.minWidth),e=this.uiDialog.css({height:"auto",width:a.width}).outerHeight(),t=Math.max(0,a.minHeight-e),i="number"==typeof a.maxHeight?Math.max(0,a.maxHeight-e):"none","auto"===a.height?this.element.css({minHeight:t,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,a.height-e)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=e(this);return e("<div>").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return e(t.target).closest(".ui-dialog").length?!0:!!e(t.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var t=this,i=this.widgetFullName;e.ui.dialog.overlayInstances||this._delay(function(){e.ui.dialog.overlayInstances&&this.document.bind("focusin.dialog",function(a){t._allowInteraction(a)||(a.preventDefault(),e(".ui-dialog:visible:last .ui-dialog-content").data(i)._focusTabbable())})}),this.overlay=e("<div>").addClass("ui-widget-overlay ui-front").appendTo(this._appendTo()),this._on(this.overlay,{mousedown:"_keepFocus"}),e.ui.dialog.overlayInstances++}},_destroyOverlay:function(){this.options.modal&&this.overlay&&(e.ui.dialog.overlayInstances--,e.ui.dialog.overlayInstances||this.document.unbind("focusin.dialog"),this.overlay.remove(),this.overlay=null)}}),e.ui.dialog.overlayInstances=0,e.uiBackCompat!==!1&&e.widget("ui.dialog",e.ui.dialog,{_position:function(){var t,i=this.options.position,a=[],s=[0,0];i?(("string"==typeof i||"object"==typeof i&&"0"in i)&&(a=i.split?i.split(" "):[i[0],i[1]],1===a.length&&(a[1]=a[0]),e.each(["left","top"],function(e,t){+a[e]===a[e]&&(s[e]=a[e],a[e]=t)}),i={my:a[0]+(0>s[0]?s[0]:"+"+s[0])+" "+a[1]+(0>s[1]?s[1]:"+"+s[1]),at:a.join(" ")}),i=e.extend({},e.ui.dialog.prototype.options.position,i)):i=e.ui.dialog.prototype.options.position,t=this.uiDialog.is(":visible"),t||this.uiDialog.show(),this.uiDialog.position(i),t||this.uiDialog.hide()}})})(jQuery);(function(t){t.widget("ui.draggable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(t(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){t("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(t(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_mouseDrag:function(e,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||t.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1):!1},_mouseUp:function(e){return t("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.element.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,t(document).width()-this.helperProportions.width-this.margins.left,(t(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(e){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=e.pageX,h=e.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,e.pageX-this.offset.click.left<i[0]&&(l=i[0]+this.offset.click.left),e.pageY-this.offset.click.top<i[1]&&(h=i[1]+this.offset.click.top),e.pageX-this.offset.click.left>i[2]&&(l=i[2]+this.offset.click.left),e.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,l=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s]),"drag"===e&&(this.positionAbs=this._convertPositionTo("absolute")),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i){var s=t(this).data("ui-draggable"),n=s.options,a=t.extend({},i,{item:s.element});s.sortables=[],t(n.connectToSortable).each(function(){var i=t.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",e,a))})},stop:function(e,i){var s=t(this).data("ui-draggable"),n=t.extend({},i,{item:s.element});t.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(e),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",e,n))})},drag:function(e,i){var s=t(this).data("ui-draggable"),n=this;t.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,t.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&t.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=t(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},e.target=this.instance.currentItem[0],this.instance._mouseCapture(e,!0),this.instance._mouseStart(e,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",e),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(e)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",e,this.instance._uiHash(this.instance)),this.instance._mouseStop(e,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",e),s.dropped=!1)})}}),t.ui.plugin.add("draggable","cursor",{start:function(){var e=t("body"),i=t(this).data("ui-draggable").options;e.css("cursor")&&(i._cursor=e.css("cursor")),e.css("cursor",i.cursor)},stop:function(){var e=t(this).data("ui-draggable").options;e._cursor&&t("body").css("cursor",e._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._opacity&&t(i.helper).css("opacity",s._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(){var e=t(this).data("ui-draggable");e.scrollParent[0]!==document&&"HTML"!==e.scrollParent[0].tagName&&(e.overflowOffset=e.scrollParent.offset())},drag:function(e){var i=t(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-e.pageY<s.scrollSensitivity?i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop+s.scrollSpeed:e.pageY-i.overflowOffset.top<s.scrollSensitivity&&(i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop-s.scrollSpeed)),s.axis&&"y"===s.axis||(i.overflowOffset.left+i.scrollParent[0].offsetWidth-e.pageX<s.scrollSensitivity?i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft+s.scrollSpeed:e.pageX-i.overflowOffset.left<s.scrollSensitivity&&(i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft-s.scrollSpeed))):(s.axis&&"x"===s.axis||(e.pageY-t(document).scrollTop()<s.scrollSensitivity?n=t(document).scrollTop(t(document).scrollTop()-s.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<s.scrollSensitivity&&(n=t(document).scrollTop(t(document).scrollTop()+s.scrollSpeed))),s.axis&&"y"===s.axis||(e.pageX-t(document).scrollLeft()<s.scrollSensitivity?n=t(document).scrollLeft(t(document).scrollLeft()-s.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<s.scrollSensitivity&&(n=t(document).scrollLeft(t(document).scrollLeft()+s.scrollSpeed)))),n!==!1&&t.ui.ddmanager&&!s.dropBehaviour&&t.ui.ddmanager.prepareOffsets(i,e)}}),t.ui.plugin.add("draggable","snap",{start:function(){var e=t(this).data("ui-draggable"),i=e.options;e.snapElements=[],t(i.snap.constructor!==String?i.snap.items||":data(ui-draggable)":i.snap).each(function(){var i=t(this),s=i.offset();this!==e.element[0]&&e.snapElements.push({item:this,width:i.outerWidth(),height:i.outerHeight(),top:s.top,left:s.left})})},drag:function(e,i){var s,n,a,o,r,l,h,c,u,d,p=t(this).data("ui-draggable"),g=p.options,f=g.snapTolerance,m=i.offset.left,_=m+p.helperProportions.width,v=i.offset.top,b=v+p.helperProportions.height;for(u=p.snapElements.length-1;u>=0;u--)r=p.snapElements[u].left,l=r+p.snapElements[u].width,h=p.snapElements[u].top,c=h+p.snapElements[u].height,r-f>_||m>l+f||h-f>b||v>c+f||!t.contains(p.snapElements[u].item.ownerDocument,p.snapElements[u].item)?(p.snapElements[u].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=!1):("inner"!==g.snapMode&&(s=f>=Math.abs(h-b),n=f>=Math.abs(c-v),a=f>=Math.abs(r-_),o=f>=Math.abs(l-m),s&&(i.position.top=p._convertPositionTo("relative",{top:h-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l}).left-p.margins.left)),d=s||n||a||o,"outer"!==g.snapMode&&(s=f>=Math.abs(h-v),n=f>=Math.abs(c-b),a=f>=Math.abs(r-m),o=f>=Math.abs(l-_),s&&(i.position.top=p._convertPositionTo("relative",{top:h,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[u].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=s||n||a||o||d)}}),t.ui.plugin.add("draggable","stack",{start:function(){var e,i=this.data("ui-draggable").options,s=t.makeArray(t(i.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});s.length&&(e=parseInt(t(s[0]).css("zIndex"),10)||0,t(s).each(function(i){t(this).css("zIndex",e+i)}),this.css("zIndex",e+s.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._zIndex&&t(i.helper).css("zIndex",s._zIndex)}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}t.widget("ui.droppable",{version:"1.10.4",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],undefined):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},t.ui.ddmanager.droppables[i.scope]=t.ui.ddmanager.droppables[i.scope]||[],t.ui.ddmanager.droppables[i.scope].push(this),i.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){for(var e=0,i=t.ui.ddmanager.droppables[this.options.scope];i.length>e;e++)i[e]===this&&i.splice(e,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(e,i){"accept"===e&&(this.accept=t.isFunction(i)?i:function(t){return t.is(i)}),t.Widget.prototype._setOption.apply(this,arguments)},_activate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var e=t.data(this,"ui-droppable");return e.options.greedy&&!e.options.disabled&&e.options.scope===s.options.scope&&e.accept.call(e.element[0],s.currentItem||s.element)&&t.ui.intersect(s,t.extend(e,{offset:e.element.offset()}),e.options.tolerance)?(n=!0,!1):undefined}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}}}),t.ui.intersect=function(t,i,s){if(!i.offset)return!1;var n,a,o=(t.positionAbs||t.position.absolute).left,r=(t.positionAbs||t.position.absolute).top,l=o+t.helperProportions.width,h=r+t.helperProportions.height,c=i.offset.left,u=i.offset.top,d=c+i.proportions().width,p=u+i.proportions().height;switch(s){case"fit":return o>=c&&d>=l&&r>=u&&p>=h;case"intersect":return o+t.helperProportions.width/2>c&&d>l-t.helperProportions.width/2&&r+t.helperProportions.height/2>u&&p>h-t.helperProportions.height/2;case"pointer":return n=(t.positionAbs||t.position.absolute).left+(t.clickOffset||t.offset.click).left,a=(t.positionAbs||t.position.absolute).top+(t.clickOffset||t.offset.click).top,e(a,u,i.proportions().height)&&e(n,c,i.proportions().width);case"touch":return(r>=u&&p>=r||h>=u&&p>=h||u>r&&h>p)&&(o>=c&&d>=o||l>=c&&d>=l||c>o&&l>d);default:return!1}},t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,a=t.ui.ddmanager.droppables[e.options.scope]||[],o=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||e&&!a[s].accept.call(a[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions().height=0;continue t}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions({width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&t.ui.intersect(e,this,this.options.tolerance)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").bind("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=t.ui.intersect(e,this,this.options.tolerance),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return t.data(this,"ui-droppable").options.scope===n}),a.length&&(s=t.data(a[0],"ui-droppable"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").unbind("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}}})(jQuery);(function(t,e){var i="ui-effects-";t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=h(),n=s._rgba=[];return i=i.toLowerCase(),f(l,function(t,a){var o,r=a.re.exec(i),l=r&&a.parse(r),h=a.space||"rgba";return l?(o=s[h](l),s[c[h].cache]=o[c[h].cache],n=s._rgba=o._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,a.transparent),s):a[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var a,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],h=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=h.support={},p=t("<p>")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,o,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=e);var u=this,d=t.type(n),p=this._rgba=[];return o!==e&&(n=[n,o,r,l],d="array"),"string"===d?this.parse(s(n)||a._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var a=s.cache;f(s.props,function(t,e){if(!u[a]&&s.to){if("alpha"===t||null==n[t])return;u[a]=s.to(u._rgba)}u[a][e.idx]=i(n[t],e,!0)}),u[a]&&0>t.inArray(null,u[a].slice(0,3))&&(u[a][3]=1,s.from&&(u._rgba=s.from(u[a])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,a){var o,r=i[a.cache];return r&&(o=n[a.cache]||a.to&&a.to(n._rgba)||[],f(a.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===o[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),a=c[n],o=0===this.alpha()?h("transparent"):this,r=o[a.cache]||a.to(o._rgba),l=r.slice();return s=s[a.cache],f(a.props,function(t,n){var a=n.idx,o=r[a],h=s[a],c=u[n.type]||{};null!==h&&(null===o?l[a]=h:(c.mod&&(h-o>c.mod/2?o+=c.mod:o-h>c.mod/2&&(o-=c.mod)),l[a]=i((h-o)*e+o,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,a=t[2]/255,o=t[3],r=Math.max(s,n,a),l=Math.min(s,n,a),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-a)/h+360:n===r?60*(a-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==o?1:o]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],a=t[3],o=.5>=s?s*(1+i):s+i-s*i,r=2*s-o;return[Math.round(255*n(r,o,e+1/3)),Math.round(255*n(r,o,e)),Math.round(255*n(r,o,e-1/3)),a]},f(c,function(s,n){var a=n.props,o=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[o]&&(this[o]=l(this._rgba)),s===e)return this[o].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[o].slice();return f(a,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[o]=d,n):h(d)},f(a,function(e,i){h.fn[e]||(h.fn[e]=function(n){var a,o=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===o?c:("function"===o&&(n=n.call(this,c),o=t.type(n)),null==n&&i.empty?this:("string"===o&&(a=r.exec(n),a&&(n=c+parseFloat(a[2])*("+"===a[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var a,o,r="";if("transparent"!==n&&("string"!==t.type(n)||(a=s(n)))){if(n=h(a||n),!d.rgba&&1!==n._rgba[3]){for(o="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&o&&o.style;)try{r=t.css(o,"backgroundColor"),o=o.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(o),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},a=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,a={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(a[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(a[i]=n[i]);return a}function s(e,i){var s,n,o={};for(s in i)n=i[s],e[s]!==n&&(a[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(o[s]=n));return o}var n=["add","remove","toggle"],a={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(jQuery.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(e,a,o,r){var l=t.speed(a,o,r);return this.queue(function(){var a,o=t(this),r=o.attr("class")||"",h=l.children?o.find("*").addBack():o;h=h.map(function(){var e=t(this);return{el:e,start:i(this)}}),a=function(){t.each(n,function(t,i){e[i]&&o[i+"Class"](e[i])})},a(),h=h.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),o.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){a(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(o[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,a){return s?t.effects.animateClass.call(this,{add:i},s,n,a):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,a){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,a):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(i){return function(s,n,a,o,r){return"boolean"==typeof n||n===e?a?t.effects.animateClass.call(this,n?{add:s}:{remove:s},a,o,r):i.apply(this,arguments):t.effects.animateClass.call(this,{toggle:s},n,a,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,a){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,a)}})}(),function(){function s(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function n(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}t.extend(t.effects,{version:"1.10.4",save:function(t,e){for(var s=0;e.length>s;s++)null!==e[s]&&t.data(i+e[s],t[0].style[e[s]])},restore:function(t,s){var n,a;for(a=0;s.length>a;a++)null!==s[a]&&(n=t.data(i+s[a]),n===e&&(n=""),t.css(s[a],n))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},a=document.activeElement;try{a.id}catch(o){a=document.body}return e.wrap(s),(e[0]===a||t.contains(e[0],a))&&t(a).focus(),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).focus()),e},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var a=e.cssUnit(i);a[0]>0&&(n[i]=a[0]*s+a[1])}),n}}),t.fn.extend({effect:function(){function e(e){function s(){t.isFunction(a)&&a.call(n[0]),t.isFunction(e)&&e()}var n=t(this),a=i.complete,r=i.mode;(n.is(":hidden")?"hide"===r:"show"===r)?(n[r](),s()):o.call(n[0],i,s)}var i=s.apply(this,arguments),n=i.mode,a=i.queue,o=t.effects.effect[i.effect];return t.fx.off||!o?n?this[n](i.duration,i.complete):this.each(function(){i.complete&&i.complete.call(this)}):a===!1?this.each(e):this.queue(a||"fx",e)},show:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(n(e))return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(n(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s}})}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}()})(jQuery);(function(t){var e=/up|down|vertical/,i=/up|left|vertical|horizontal/;t.effects.effect.blind=function(s,n){var a,o,r,l=t(this),h=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(l,s.mode||"hide"),u=s.direction||"up",d=e.test(u),p=d?"height":"width",f=d?"top":"left",g=i.test(u),m={},v="show"===c;l.parent().is(".ui-effects-wrapper")?t.effects.save(l.parent(),h):t.effects.save(l,h),l.show(),a=t.effects.createWrapper(l).css({overflow:"hidden"}),o=a[p](),r=parseFloat(a.css(f))||0,m[p]=v?o:0,g||(l.css(d?"bottom":"right",0).css(d?"top":"left","auto").css({position:"absolute"}),m[f]=v?r:o+r),v&&(a.css(p,0),g||a.css(f,r+o)),a.animate(m,{duration:s.duration,easing:s.easing,queue:!1,complete:function(){"hide"===c&&l.hide(),t.effects.restore(l,h),t.effects.removeWrapper(l),n()}})}})(jQuery);(function(t){t.effects.effect.bounce=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"effect"),h="hide"===l,c="show"===l,u=e.direction||"up",d=e.distance,p=e.times||5,f=2*p+(c||h?1:0),g=e.duration/f,m=e.easing,v="up"===u||"down"===u?"top":"left",_="up"===u||"left"===u,b=o.queue(),y=b.length;for((c||h)&&r.push("opacity"),t.effects.save(o,r),o.show(),t.effects.createWrapper(o),d||(d=o["top"===v?"outerHeight":"outerWidth"]()/3),c&&(a={opacity:1},a[v]=0,o.css("opacity",0).css(v,_?2*-d:2*d).animate(a,g,m)),h&&(d/=Math.pow(2,p-1)),a={},a[v]=0,s=0;p>s;s++)n={},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m).animate(a,g,m),d=h?2*d:d/2;h&&(n={opacity:0},n[v]=(_?"-=":"+=")+d,o.animate(n,g,m)),o.queue(function(){h&&o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}),y>1&&b.splice.apply(b,[1,0].concat(b.splice(y,f+1))),o.dequeue()}})(jQuery);(function(t){t.effects.effect.clip=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","height","width"],l=t.effects.setMode(o,e.mode||"hide"),h="show"===l,c=e.direction||"vertical",u="vertical"===c,d=u?"height":"width",p=u?"top":"left",f={};t.effects.save(o,r),o.show(),s=t.effects.createWrapper(o).css({overflow:"hidden"}),n="IMG"===o[0].tagName?s:o,a=n[d](),h&&(n.css(d,0),n.css(p,a/2)),f[d]=h?a:0,f[p]=h?0:a/2,n.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){h||o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.drop=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","opacity","height","width"],o=t.effects.setMode(n,e.mode||"hide"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l?"pos":"neg",u={opacity:r?1:0};t.effects.save(n,a),n.show(),t.effects.createWrapper(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,r&&n.css("opacity",0).css(h,"pos"===c?-s:s),u[h]=(r?"pos"===c?"+=":"-=":"pos"===c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.explode=function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),g||p.hide(),i()}var a,o,r,l,h,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),g="show"===f,m=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/d),_=Math.ceil(p.outerHeight()/u),b=[];for(a=0;u>a;a++)for(l=m.top+a*_,c=a-(u-1)/2,o=0;d>o;o++)r=m.left+o*v,h=o-(d-1)/2,p.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-o*v,top:-a*_}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:_,left:r+(g?h*v:0),top:l+(g?c*_:0),opacity:g?0:1}).animate({left:r+(g?0:h*v),top:l+(g?0:c*_),opacity:g?1:0},e.duration||500,e.easing,s)}})(jQuery);(function(t){t.effects.effect.fade=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"toggle");s.animate({opacity:n},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}})(jQuery);(function(t){t.effects.effect.fold=function(e,i){var s,n,a=t(this),o=["position","top","bottom","left","right","height","width"],r=t.effects.setMode(a,e.mode||"hide"),l="show"===r,h="hide"===r,c=e.size||15,u=/([0-9]+)%/.exec(c),d=!!e.horizFirst,p=l!==d,f=p?["width","height"]:["height","width"],g=e.duration/2,m={},v={};t.effects.save(a,o),a.show(),s=t.effects.createWrapper(a).css({overflow:"hidden"}),n=p?[s.width(),s.height()]:[s.height(),s.width()],u&&(c=parseInt(u[1],10)/100*n[h?0:1]),l&&s.css(d?{height:0,width:c}:{height:c,width:0}),m[f[0]]=l?n[0]:c,v[f[1]]=l?n[1]:0,s.animate(m,g,e.easing).animate(v,g,e.easing,function(){h&&a.hide(),t.effects.restore(a,o),t.effects.removeWrapper(a),i()})}})(jQuery);(function(t){t.effects.effect.highlight=function(e,i){var s=t(this),n=["backgroundImage","backgroundColor","opacity"],a=t.effects.setMode(s,e.mode||"show"),o={backgroundColor:s.css("backgroundColor")};"hide"===a&&(o.opacity=0),t.effects.save(s,n),s.show().css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(o,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===a&&s.hide(),t.effects.restore(s,n),i()}})}})(jQuery);(function(t){t.effects.effect.pulsate=function(e,i){var s,n=t(this),a=t.effects.setMode(n,e.mode||"show"),o="show"===a,r="hide"===a,l=o||"hide"===a,h=2*(e.times||5)+(l?1:0),c=e.duration/h,u=0,d=n.queue(),p=d.length;for((o||!n.is(":visible"))&&(n.css("opacity",0).show(),u=1),s=1;h>s;s++)n.animate({opacity:u},c,e.easing),u=1-u;n.animate({opacity:u},c,e.easing),n.queue(function(){r&&n.hide(),i()}),p>1&&d.splice.apply(d,[1,0].concat(d.splice(p,h+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.puff=function(e,i){var s=t(this),n=t.effects.setMode(s,e.mode||"hide"),a="hide"===n,o=parseInt(e.percent,10)||150,r=o/100,l={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()};t.extend(e,{effect:"scale",queue:!1,fade:!0,mode:n,complete:i,percent:a?o:100,from:a?l:{height:l.height*r,width:l.width*r,outerHeight:l.outerHeight*r,outerWidth:l.outerWidth*r}}),s.effect(e)},t.effects.effect.scale=function(e,i){var s=t(this),n=t.extend(!0,{},e),a=t.effects.setMode(s,e.mode||"effect"),o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"hide"===a?0:100),r=e.direction||"both",l=e.origin,h={height:s.height(),width:s.width(),outerHeight:s.outerHeight(),outerWidth:s.outerWidth()},c={y:"horizontal"!==r?o/100:1,x:"vertical"!==r?o/100:1};n.effect="size",n.queue=!1,n.complete=i,"effect"!==a&&(n.origin=l||["middle","center"],n.restore=!0),n.from=e.from||("show"===a?{height:0,width:0,outerHeight:0,outerWidth:0}:h),n.to={height:h.height*c.y,width:h.width*c.x,outerHeight:h.outerHeight*c.y,outerWidth:h.outerWidth*c.x},n.fade&&("show"===a&&(n.from.opacity=0,n.to.opacity=1),"hide"===a&&(n.from.opacity=1,n.to.opacity=0)),s.effect(n)},t.effects.effect.size=function(e,i){var s,n,a,o=t(this),r=["position","top","bottom","left","right","width","height","overflow","opacity"],l=["position","top","bottom","left","right","overflow","opacity"],h=["width","height","overflow"],c=["fontSize"],u=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],d=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=t.effects.setMode(o,e.mode||"effect"),f=e.restore||"effect"!==p,g=e.scale||"both",m=e.origin||["middle","center"],v=o.css("position"),_=f?r:l,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&o.show(),s={height:o.height(),width:o.width(),outerHeight:o.outerHeight(),outerWidth:o.outerWidth()},"toggle"===e.mode&&"show"===p?(o.from=e.to||b,o.to=e.from||s):(o.from=e.from||("show"===p?b:s),o.to=e.to||("hide"===p?b:s)),a={from:{y:o.from.height/s.height,x:o.from.width/s.width},to:{y:o.to.height/s.height,x:o.to.width/s.width}},("box"===g||"both"===g)&&(a.from.y!==a.to.y&&(_=_.concat(u),o.from=t.effects.setTransition(o,u,a.from.y,o.from),o.to=t.effects.setTransition(o,u,a.to.y,o.to)),a.from.x!==a.to.x&&(_=_.concat(d),o.from=t.effects.setTransition(o,d,a.from.x,o.from),o.to=t.effects.setTransition(o,d,a.to.x,o.to))),("content"===g||"both"===g)&&a.from.y!==a.to.y&&(_=_.concat(c).concat(h),o.from=t.effects.setTransition(o,c,a.from.y,o.from),o.to=t.effects.setTransition(o,c,a.to.y,o.to)),t.effects.save(o,_),o.show(),t.effects.createWrapper(o),o.css("overflow","hidden").css(o.from),m&&(n=t.effects.getBaseline(m,s),o.from.top=(s.outerHeight-o.outerHeight())*n.y,o.from.left=(s.outerWidth-o.outerWidth())*n.x,o.to.top=(s.outerHeight-o.to.outerHeight)*n.y,o.to.left=(s.outerWidth-o.to.outerWidth)*n.x),o.css(o.from),("content"===g||"both"===g)&&(u=u.concat(["marginTop","marginBottom"]).concat(c),d=d.concat(["marginLeft","marginRight"]),h=r.concat(u).concat(d),o.find("*[width]").each(function(){var i=t(this),s={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()};f&&t.effects.save(i,h),i.from={height:s.height*a.from.y,width:s.width*a.from.x,outerHeight:s.outerHeight*a.from.y,outerWidth:s.outerWidth*a.from.x},i.to={height:s.height*a.to.y,width:s.width*a.to.x,outerHeight:s.height*a.to.y,outerWidth:s.width*a.to.x},a.from.y!==a.to.y&&(i.from=t.effects.setTransition(i,u,a.from.y,i.from),i.to=t.effects.setTransition(i,u,a.to.y,i.to)),a.from.x!==a.to.x&&(i.from=t.effects.setTransition(i,d,a.from.x,i.from),i.to=t.effects.setTransition(i,d,a.to.x,i.to)),i.css(i.from),i.animate(i.to,e.duration,e.easing,function(){f&&t.effects.restore(i,h)})})),o.animate(o.to,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){0===o.to.opacity&&o.css("opacity",o.from.opacity),"hide"===p&&o.hide(),t.effects.restore(o,_),f||("static"===v?o.css({position:"relative",top:o.to.top,left:o.to.left}):t.each(["top","left"],function(t,e){o.css(e,function(e,i){var s=parseInt(i,10),n=t?o.to.left:o.to.top;return"auto"===i?n+"px":s+n+"px"})})),t.effects.removeWrapper(o),i()}})}})(jQuery);(function(t){t.effects.effect.shake=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","height","width"],o=t.effects.setMode(n,e.mode||"effect"),r=e.direction||"left",l=e.distance||20,h=e.times||3,c=2*h+1,u=Math.round(e.duration/c),d="up"===r||"down"===r?"top":"left",p="up"===r||"left"===r,f={},g={},m={},v=n.queue(),_=v.length;for(t.effects.save(n,a),n.show(),t.effects.createWrapper(n),f[d]=(p?"-=":"+=")+l,g[d]=(p?"+=":"-=")+2*l,m[d]=(p?"-=":"+=")+2*l,n.animate(f,u,e.easing),s=1;h>s;s++)n.animate(g,u,e.easing).animate(m,u,e.easing);n.animate(g,u,e.easing).animate(f,u/2,e.easing).queue(function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}),_>1&&v.splice.apply(v,[1,0].concat(v.splice(_,c+1))),n.dequeue()}})(jQuery);(function(t){t.effects.effect.slide=function(e,i){var s,n=t(this),a=["position","top","bottom","left","right","width","height"],o=t.effects.setMode(n,e.mode||"show"),r="show"===o,l=e.direction||"left",h="up"===l||"down"===l?"top":"left",c="up"===l||"left"===l,u={};t.effects.save(n,a),n.show(),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0),t.effects.createWrapper(n).css({overflow:"hidden"}),r&&n.css(h,c?isNaN(s)?"-"+s:-s:s),u[h]=(r?c?"+=":"-=":c?"-=":"+=")+s,n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===o&&n.hide(),t.effects.restore(n,a),t.effects.removeWrapper(n),i()}})}})(jQuery);(function(t){t.effects.effect.transfer=function(e,i){var s=t(this),n=t(e.to),a="fixed"===n.css("position"),o=t("body"),r=a?o.scrollTop():0,l=a?o.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("<div class='ui-effects-transfer'></div>").appendTo(document.body).addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:a?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),i()})}})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("<span>").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)}})})(jQuery);(function(t,e){t.widget("ui.progressbar",{version:"1.10.4",options:{max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min}),this.valueDiv=t("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(t){return t===e?this.options.value:(this.options.value=this._constrainedValue(t),this._refreshValue(),e)},_constrainedValue:function(t){return t===e&&(t=this.options.value),this.indeterminate=t===!1,"number"!=typeof t&&(t=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var e=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(i.toFixed(0)+"%"),this.element.toggleClass("ui-progressbar-indeterminate",this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=t("<div class='ui-progressbar-overlay'></div>").appendTo(this.valueDiv))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":e}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),e===this.options.max&&this._trigger("complete")}})})(jQuery);(function(t){function e(t){return parseInt(t,10)||0}function i(t){return!isNaN(parseInt(t,10))}t.widget("ui.resizable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_create:function(){var e,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(t("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.data("ui-resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),e=this.handles.split(","),this.handles={},i=0;e.length>i;i++)s=t.trim(e[i]),a="ui-resizable-"+s,n=t("<div class='ui-resizable-handle "+a+"'></div>"),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(e){var i,s,n,a;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=t(this.handles[i],this.element).show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=t(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,a),this._proportionallyResize()),t(this.handles[i]).length},this._renderAxis(this.element),this._handles=t(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),t(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(t(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(t(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(i){var s,n,a,o=this.options,r=this.element.position(),h=this.element;return this.resizing=!0,/absolute/.test(h.css("position"))?h.css({position:"absolute",top:h.css("top"),left:h.css("left")}):h.is(".ui-draggable")&&h.css({position:"absolute",top:r.top,left:r.left}),this._renderProxy(),s=e(this.helper.css("left")),n=e(this.helper.css("top")),o.containment&&(s+=t(o.containment).scrollLeft()||0,n+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:s,top:n},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:h.width(),height:h.height()},this.originalSize=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalPosition={left:s,top:n},this.sizeDiff={width:h.outerWidth()-h.width(),height:h.outerHeight()-h.height()},this.originalMousePosition={left:i.pageX,top:i.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,a=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===a?this.axis+"-resize":a),h.addClass("ui-resizable-resizing"),this._propagate("start",i),!0},_mouseDrag:function(e){var i,s=this.helper,n={},a=this.originalMousePosition,o=this.axis,r=this.position.top,h=this.position.left,l=this.size.width,c=this.size.height,u=e.pageX-a.left||0,d=e.pageY-a.top||0,p=this._change[o];return p?(i=p.apply(this,[e,u,d]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),this.position.top!==r&&(n.top=this.position.top+"px"),this.position.left!==h&&(n.left=this.position.left+"px"),this.size.width!==l&&(n.width=this.size.width+"px"),this.size.height!==c&&(n.height=this.size.height+"px"),s.css(n),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(n)||this._trigger("resize",e,this.ui()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&t.ui.hasScroll(i[0],"left")?0:c.sizeDiff.height,a=s?0:c.sizeDiff.width,o={width:c.helper.width()-a,height:c.helper.height()-n},r=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null,h=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(o,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(t){var e,s,n,a,o,r=this.options;o={minWidth:i(r.minWidth)?r.minWidth:0,maxWidth:i(r.maxWidth)?r.maxWidth:1/0,minHeight:i(r.minHeight)?r.minHeight:0,maxHeight:i(r.maxHeight)?r.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,n=o.minWidth/this.aspectRatio,s=o.maxHeight*this.aspectRatio,a=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),n>o.minHeight&&(o.minHeight=n),o.maxWidth>s&&(o.maxWidth=s),o.maxHeight>a&&(o.maxHeight=a)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),i(t.left)&&(this.position.left=t.left),i(t.top)&&(this.position.top=t.top),i(t.height)&&(this.size.height=t.height),i(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,s=this.size,n=this.axis;return i(t.height)?t.width=t.height*this.aspectRatio:i(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===n&&(t.left=e.left+(s.width-t.width),t.top=null),"nw"===n&&(t.top=e.top+(s.height-t.height),t.left=e.left+(s.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,s=this.axis,n=i(t.width)&&e.maxWidth&&e.maxWidth<t.width,a=i(t.height)&&e.maxHeight&&e.maxHeight<t.height,o=i(t.width)&&e.minWidth&&e.minWidth>t.width,r=i(t.height)&&e.minHeight&&e.minHeight>t.height,h=this.originalPosition.left+this.originalSize.width,l=this.position.top+this.size.height,c=/sw|nw|w/.test(s),u=/nw|ne|n/.test(s);return o&&(t.width=e.minWidth),r&&(t.height=e.minHeight),n&&(t.width=e.maxWidth),a&&(t.height=e.maxHeight),o&&c&&(t.left=h-e.minWidth),n&&c&&(t.left=h-e.maxWidth),r&&u&&(t.top=l-e.minHeight),a&&u&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_proportionallyResize:function(){if(this._proportionallyResizeElements.length){var t,e,i,s,n,a=this.helper||this.element;for(t=0;this._proportionallyResizeElements.length>t;t++){if(n=this._proportionallyResizeElements[t],!this.borderDif)for(this.borderDif=[],i=[n.css("borderTopWidth"),n.css("borderRightWidth"),n.css("borderBottomWidth"),n.css("borderLeftWidth")],s=[n.css("paddingTop"),n.css("paddingRight"),n.css("paddingBottom"),n.css("paddingLeft")],e=0;i.length>e;e++)this.borderDif[e]=(parseInt(i[e],10)||0)+(parseInt(s[e],10)||0);n.css({height:a.height()-this.borderDif[0]-this.borderDif[2]||0,width:a.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("<div style='overflow:hidden;'></div>"),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).data("ui-resizable"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&t.ui.hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,c=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var i,s,n,a,o,r,h,l=t(this).data("ui-resizable"),c=l.options,u=l.element,d=c.containment,p=d instanceof t?d.get(0):/parent/.test(d)?u.parent().get(0):d;p&&(l.containerElement=t(p),/document/.test(d)||d===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(i=t(p),s=[],t(["Top","Right","Left","Bottom"]).each(function(t,n){s[t]=e(i.css("padding"+n))}),l.containerOffset=i.offset(),l.containerPosition=i.position(),l.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},n=l.containerOffset,a=l.containerSize.height,o=l.containerSize.width,r=t.ui.hasScroll(p,"left")?p.scrollWidth:o,h=t.ui.hasScroll(p)?p.scrollHeight:a,l.parentData={element:p,left:n.left,top:n.top,width:r,height:h}))},resize:function(e){var i,s,n,a,o=t(this).data("ui-resizable"),r=o.options,h=o.containerOffset,l=o.position,c=o._aspectRatio||e.shiftKey,u={top:0,left:0},d=o.containerElement;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-u.left),c&&(o.size.height=o.size.width/o.aspectRatio),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),c&&(o.size.width=o.size.height*o.aspectRatio),o.position.top=o._helper?h.top:0),o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top,i=Math.abs((o._helper?o.offset.left-u.left:o.offset.left-u.left)+o.sizeDiff.width),s=Math.abs((o._helper?o.offset.top-u.top:o.offset.top-h.top)+o.sizeDiff.height),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a&&(i-=Math.abs(o.parentData.left)),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,c&&(o.size.height=o.size.width/o.aspectRatio)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,c&&(o.size.width=o.size.height*o.aspectRatio))},stop:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.containerOffset,n=e.containerPosition,a=e.containerElement,o=t(e.helper),r=o.offset(),h=o.outerWidth()-e.sizeDiff.width,l=o.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=function(e){t(e).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseInt(e.width(),10),height:parseInt(e.height(),10),left:parseInt(e.css("left"),10),top:parseInt(e.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):t.each(i.alsoResize,function(t){s(t)})},resize:function(e,i){var s=t(this).data("ui-resizable"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(e,s){t(e).each(function(){var e=t(this),n=t(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(n[e]||0)+(r[e]||0);i&&i>=0&&(a[e]=i||null)}),e.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):t.each(n.alsoResize,function(t,e){h(t,e)})},stop:function(){t(this).removeData("resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).data("ui-resizable");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).data("ui-resizable");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size,n=e.originalSize,a=e.originalPosition,o=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,h=r[0]||1,l=r[1]||1,c=Math.round((s.width-n.width)/h)*h,u=Math.round((s.height-n.height)/l)*l,d=n.width+c,p=n.height+u,f=i.maxWidth&&d>i.maxWidth,g=i.maxHeight&&p>i.maxHeight,m=i.minWidth&&i.minWidth>d,v=i.minHeight&&i.minHeight>p;i.grid=r,m&&(d+=h),v&&(p+=l),f&&(d-=h),g&&(p-=l),/^(se|s|e)$/.test(o)?(e.size.width=d,e.size.height=p):/^(ne)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.top=a.top-u):/^(sw)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.left=a.left-c):(p-l>0?(e.size.height=p,e.position.top=a.top-u):(e.size.height=l,e.position.top=a.top+n.height-l),d-h>0?(e.size.width=d,e.position.left=a.left-c):(e.size.width=h,e.position.left=a.left+n.width-h))}})})(jQuery);(function(t){t.widget("ui.selectable",t.ui.mouse,{version:"1.10.4",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e,i=this;this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e=t(i.options.filter,i.element[0]),e.addClass("ui-selectee"),e.each(function(){var e=t(this),i=e.offset();t.data(this,"selectable-item",{element:this,$element:e,left:i.left,top:i.top,right:i.left+e.outerWidth(),bottom:i.top+e.outerHeight(),startselected:!1,selected:e.hasClass("ui-selected"),selecting:e.hasClass("ui-selecting"),unselecting:e.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=e.addClass("ui-selectee"),this._mouseInit(),this.helper=t("<div class='ui-selectable-helper'></div>")},_destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(s.$element.removeClass("ui-selected"),s.selected=!1,s.$element.addClass("ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),n.$element.removeClass(s?"ui-unselecting":"ui-selected").addClass(s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):undefined}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,a=this.opos[0],o=this.opos[1],r=e.pageX,l=e.pageY;return a>r&&(i=r,r=a,a=i),o>l&&(i=l,l=o,o=i),this.helper.css({left:a,top:o,width:r-a,height:l-o}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1;i&&i.element!==s.element[0]&&("touch"===n.tolerance?h=!(i.left>r||a>i.right||i.top>l||o>i.bottom):"fit"===n.tolerance&&(h=i.left>a&&r>i.right&&i.top>o&&l>i.bottom),h?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");s.$element.removeClass("ui-selecting").addClass("ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}})})(jQuery);(function(t){var e=5;t.widget("ui.slider",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null,change:null,slide:null,start:null,stop:null},_create:function(){this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"),this._refresh(),this._setOption("disabled",this.options.disabled),this._animateOff=!1},_refresh:function(){this._createRange(),this._createHandles(),this._setupEvents(),this._refreshValue()},_createHandles:function(){var e,i,s=this.options,n=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),a="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",o=[];for(i=s.values&&s.values.length||1,n.length>i&&(n.slice(i).remove(),n=n.slice(0,i)),e=n.length;i>e;e++)o.push(a);this.handles=n.add(t(o.join("")).appendTo(this.element)),this.handle=this.handles.eq(0),this.handles.each(function(e){t(this).data("ui-slider-handle-index",e)})},_createRange:function(){var e=this.options,i="";e.range?(e.range===!0&&(e.values?e.values.length&&2!==e.values.length?e.values=[e.values[0],e.values[0]]:t.isArray(e.values)&&(e.values=e.values.slice(0)):e.values=[this._valueMin(),this._valueMin()]),this.range&&this.range.length?this.range.removeClass("ui-slider-range-min ui-slider-range-max").css({left:"",bottom:""}):(this.range=t("<div></div>").appendTo(this.element),i="ui-slider-range ui-widget-header ui-corner-all"),this.range.addClass(i+("min"===e.range||"max"===e.range?" ui-slider-range-"+e.range:""))):(this.range&&this.range.remove(),this.range=null)},_setupEvents:function(){var t=this.handles.add(this.range).filter("a");this._off(t),this._on(t,this._handleEvents),this._hoverable(t),this._focusable(t)},_destroy:function(){this.handles.remove(),this.range&&this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-widget ui-widget-content ui-corner-all"),this._mouseDestroy()},_mouseCapture:function(e){var i,s,n,a,o,r,l,h,u=this,c=this.options;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),i={x:e.pageX,y:e.pageY},s=this._normValueFromMouse(i),n=this._valueMax()-this._valueMin()+1,this.handles.each(function(e){var i=Math.abs(s-u.values(e));(n>i||n===i&&(e===u._lastChangedValue||u.values(e)===c.min))&&(n=i,a=t(this),o=e)}),r=this._start(e,o),r===!1?!1:(this._mouseSliding=!0,this._handleIndex=o,a.addClass("ui-state-active").focus(),l=a.offset(),h=!t(e.target).parents().addBack().is(".ui-slider-handle"),this._clickOffset=h?{left:0,top:0}:{left:e.pageX-l.left-a.width()/2,top:e.pageY-l.top-a.height()/2-(parseInt(a.css("borderTopWidth"),10)||0)-(parseInt(a.css("borderBottomWidth"),10)||0)+(parseInt(a.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(e,o,s),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(t){var e={x:t.pageX,y:t.pageY},i=this._normValueFromMouse(e);return this._slide(t,this._handleIndex,i),!1},_mouseStop:function(t){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(t,this._handleIndex),this._change(t,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation="vertical"===this.options.orientation?"vertical":"horizontal"},_normValueFromMouse:function(t){var e,i,s,n,a;return"horizontal"===this.orientation?(e=this.elementSize.width,i=t.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(e=this.elementSize.height,i=t.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),s=i/e,s>1&&(s=1),0>s&&(s=0),"vertical"===this.orientation&&(s=1-s),n=this._valueMax()-this._valueMin(),a=this._valueMin()+s*n,this._trimAlignValue(a)},_start:function(t,e){var i={handle:this.handles[e],value:this.value()};return this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("start",t,i)},_slide:function(t,e,i){var s,n,a;this.options.values&&this.options.values.length?(s=this.values(e?0:1),2===this.options.values.length&&this.options.range===!0&&(0===e&&i>s||1===e&&s>i)&&(i=s),i!==this.values(e)&&(n=this.values(),n[e]=i,a=this._trigger("slide",t,{handle:this.handles[e],value:i,values:n}),s=this.values(e?0:1),a!==!1&&this.values(e,i))):i!==this.value()&&(a=this._trigger("slide",t,{handle:this.handles[e],value:i}),a!==!1&&this.value(i))},_stop:function(t,e){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._trigger("stop",t,i)},_change:function(t,e){if(!this._keySliding&&!this._mouseSliding){var i={handle:this.handles[e],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(e),i.values=this.values()),this._lastChangedValue=e,this._trigger("change",t,i)}},value:function(t){return arguments.length?(this.options.value=this._trimAlignValue(t),this._refreshValue(),this._change(null,0),undefined):this._value()},values:function(e,i){var s,n,a;if(arguments.length>1)return this.options.values[e]=this._trimAlignValue(i),this._refreshValue(),this._change(null,e),undefined;if(!arguments.length)return this._values();if(!t.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(e):this.value();for(s=this.options.values,n=arguments[0],a=0;s.length>a;a+=1)s[a]=this._trimAlignValue(n[a]),this._change(null,a);this._refreshValue()},_setOption:function(e,i){var s,n=0;switch("range"===e&&this.options.range===!0&&("min"===i?(this.options.value=this._values(0),this.options.values=null):"max"===i&&(this.options.value=this._values(this.options.values.length-1),this.options.values=null)),t.isArray(this.options.values)&&(n=this.options.values.length),t.Widget.prototype._setOption.apply(this,arguments),e){case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":for(this._animateOff=!0,this._refreshValue(),s=0;n>s;s+=1)this._change(null,s);this._animateOff=!1;break;case"min":case"max":this._animateOff=!0,this._refreshValue(),this._animateOff=!1;break;case"range":this._animateOff=!0,this._refresh(),this._animateOff=!1}},_value:function(){var t=this.options.value;return t=this._trimAlignValue(t)},_values:function(t){var e,i,s;if(arguments.length)return e=this.options.values[t],e=this._trimAlignValue(e);if(this.options.values&&this.options.values.length){for(i=this.options.values.slice(),s=0;i.length>s;s+=1)i[s]=this._trimAlignValue(i[s]);return i}return[]},_trimAlignValue:function(t){if(this._valueMin()>=t)return this._valueMin();if(t>=this._valueMax())return this._valueMax();var e=this.options.step>0?this.options.step:1,i=(t-this._valueMin())%e,s=t-i;return 2*Math.abs(i)>=e&&(s+=i>0?e:-e),parseFloat(s.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var e,i,s,n,a,o=this.options.range,r=this.options,l=this,h=this._animateOff?!1:r.animate,u={};this.options.values&&this.options.values.length?this.handles.each(function(s){i=100*((l.values(s)-l._valueMin())/(l._valueMax()-l._valueMin())),u["horizontal"===l.orientation?"left":"bottom"]=i+"%",t(this).stop(1,1)[h?"animate":"css"](u,r.animate),l.options.range===!0&&("horizontal"===l.orientation?(0===s&&l.range.stop(1,1)[h?"animate":"css"]({left:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({width:i-e+"%"},{queue:!1,duration:r.animate})):(0===s&&l.range.stop(1,1)[h?"animate":"css"]({bottom:i+"%"},r.animate),1===s&&l.range[h?"animate":"css"]({height:i-e+"%"},{queue:!1,duration:r.animate}))),e=i}):(s=this.value(),n=this._valueMin(),a=this._valueMax(),i=a!==n?100*((s-n)/(a-n)):0,u["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[h?"animate":"css"](u,r.animate),"min"===o&&"horizontal"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({width:i+"%"},r.animate),"max"===o&&"horizontal"===this.orientation&&this.range[h?"animate":"css"]({width:100-i+"%"},{queue:!1,duration:r.animate}),"min"===o&&"vertical"===this.orientation&&this.range.stop(1,1)[h?"animate":"css"]({height:i+"%"},r.animate),"max"===o&&"vertical"===this.orientation&&this.range[h?"animate":"css"]({height:100-i+"%"},{queue:!1,duration:r.animate}))},_handleEvents:{keydown:function(i){var s,n,a,o,r=t(i.target).data("ui-slider-handle-index");switch(i.keyCode){case t.ui.keyCode.HOME:case t.ui.keyCode.END:case t.ui.keyCode.PAGE_UP:case t.ui.keyCode.PAGE_DOWN:case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(i.preventDefault(),!this._keySliding&&(this._keySliding=!0,t(i.target).addClass("ui-state-active"),s=this._start(i,r),s===!1))return}switch(o=this.options.step,n=a=this.options.values&&this.options.values.length?this.values(r):this.value(),i.keyCode){case t.ui.keyCode.HOME:a=this._valueMin();break;case t.ui.keyCode.END:a=this._valueMax();break;case t.ui.keyCode.PAGE_UP:a=this._trimAlignValue(n+(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.PAGE_DOWN:a=this._trimAlignValue(n-(this._valueMax()-this._valueMin())/e);break;case t.ui.keyCode.UP:case t.ui.keyCode.RIGHT:if(n===this._valueMax())return;a=this._trimAlignValue(n+o);break;case t.ui.keyCode.DOWN:case t.ui.keyCode.LEFT:if(n===this._valueMin())return;a=this._trimAlignValue(n-o)}this._slide(i,r,a)},click:function(t){t.preventDefault()},keyup:function(e){var i=t(e.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(e,i),this._change(e,i),t(e.target).removeClass("ui-state-active"))}}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):undefined}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("<style>*{ cursor: "+a.cursor+" !important; }</style>").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<a.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+a.scrollSpeed:e.pageY-this.overflowOffset.top<a.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-a.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<a.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+a.scrollSpeed:e.pageX-this.overflowOffset.left<a.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-a.scrollSpeed)):(e.pageY-t(document).scrollTop()<a.scrollSensitivity?r=t(document).scrollTop(t(document).scrollTop()-a.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<a.scrollSensitivity&&(r=t(document).scrollTop(t(document).scrollTop()+a.scrollSpeed)),e.pageX-t(document).scrollLeft()<a.scrollSensitivity?r=t(document).scrollLeft(t(document).scrollLeft()-a.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<a.scrollSensitivity&&(r=t(document).scrollLeft(t(document).scrollLeft()+a.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t("<td>&#160;</td>",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(o=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(a=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(t){function e(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.widget("ui.spinner",{version:"1.10.4",defaultElement:"<input>",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e={},i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);void 0!==n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var t=this.element[0]===this.document[0].activeElement;t||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var t=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=t.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*t.height())&&t.height()>0&&t.height(t.height()),this.options.disabled&&this.disable()},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_uiSpinnerHtml:function(){return"<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>"},_buttonHtml:function(){return"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'><span class='ui-icon "+this.options.icons.up+"'>&#9650;</span>"+"</a>"+"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>"+"<span class='ui-icon "+this.options.icons.down+"'>&#9660;</span>"+"</a>"},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){if("culture"===t||"numberFormat"===t){var i=this._parse(this.element.val());return this.options[t]=e,this.element.val(this._format(i)),void 0}("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(this.buttons.first().find(".ui-icon").removeClass(this.options.icons.up).addClass(e.up),this.buttons.last().find(".ui-icon").removeClass(this.options.icons.down).addClass(e.down)),this._super(t,e),"disabled"===t&&(e?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:e(function(t){this._super(t),this._value(this.element.val())}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:e(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:e(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:e(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:e(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(e(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}})})(jQuery);(function(t,e){function i(){return++n}function s(t){return t=t.cloneNode(!1),t.hash.length>1&&decodeURIComponent(t.href.replace(a,""))===decodeURIComponent(location.href.replace(a,""))}var n=0,a=/#.*$/;t.widget("ui.tabs",{version:"1.10.4",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var e=this,i=this.options;this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",i.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var i=this.options.active,s=this.options.collapsible,n=location.hash.substring(1);return null===i&&(n&&this.tabs.each(function(s,a){return t(a).attr("aria-controls")===n?(i=s,!1):e}),null===i&&(i=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===i||-1===i)&&(i=this.tabs.length?0:!1)),i!==!1&&(i=this.tabs.index(this.tabs.eq(i)),-1===i&&(i=s?!1:0)),!s&&i===!1&&this.anchors.length&&(i=0),i},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(i){var s=t(this.document[0].activeElement).closest("li"),n=this.tabs.index(s),a=!0;if(!this._handlePageNav(i)){switch(i.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:n++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:a=!1,n--;break;case t.ui.keyCode.END:n=this.anchors.length-1;break;case t.ui.keyCode.HOME:n=0;break;case t.ui.keyCode.SPACE:return i.preventDefault(),clearTimeout(this.activating),this._activate(n),e;case t.ui.keyCode.ENTER:return i.preventDefault(),clearTimeout(this.activating),this._activate(n===this.options.active?!1:n),e;default:return}i.preventDefault(),clearTimeout(this.activating),n=this._focusNextTab(n,a),i.ctrlKey||(s.attr("aria-selected","false"),this.tabs.eq(n).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",n)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.focus())},_handlePageNav:function(i){return i.altKey&&i.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):i.altKey&&i.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):e},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).focus(),t},_setOption:function(t,i){return"active"===t?(this._activate(i),e):"disabled"===t?(this._setupDisabled(i),e):(this._super(t,i),"collapsible"===t&&(this.element.toggleClass("ui-tabs-collapsible",i),i||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(i),"heightStyle"===t&&this._setupHeightStyle(i),e)},_tabId:function(t){return t.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=t(),this.anchors.each(function(i,n){var a,o,r,h=t(n).uniqueId().attr("id"),l=t(n).closest("li"),c=l.attr("aria-controls");s(n)?(a=n.hash,o=e.element.find(e._sanitizeSelector(a))):(r=e._tabId(l),a="#"+r,o=e.element.find(a),o.length||(o=e._createPanel(r),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),c&&l.data("ui-tabs-aria-controls",c),l.attr({"aria-controls":a.substring(1),"aria-labelledby":h}),o.attr("aria-labelledby",h)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.tablist||this.element.find("ol,ul").eq(0)},_createPanel:function(e){return t("<div>").attr("id",e).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(e){t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1);for(var i,s=0;i=this.tabs[s];s++)e===!0||-1!==t.inArray(s,e)?t(i).addClass("ui-state-disabled").attr("aria-disabled","true"):t(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=e},_setupEvents:function(e){var i={click:function(t){t.preventDefault()}};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),a=n.closest("li"),o=a[0]===s[0],r=o&&i.collapsible,h=r?t():this._getPanelForTab(a),l=s.length?this._getPanelForTab(s):t(),c={oldTab:s,oldPanel:l,newTab:r?t():a,newPanel:h};e.preventDefault(),a.hasClass("ui-state-disabled")||a.hasClass("ui-tabs-loading")||this.running||o&&!i.collapsible||this._trigger("beforeActivate",e,c)===!1||(i.active=r?!1:this.tabs.index(a),this.active=o?t():a,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(a),e),this._toggle(e,c))},_toggle:function(e,i){function s(){a.running=!1,a._trigger("activate",e,i)}function n(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),o.length&&a.options.show?a._show(o,a.options.show,s):(o.show(),s())}var a=this,o=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),n()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),r.hide(),n()),r.attr({"aria-expanded":"false","aria-hidden":"true"}),i.oldTab.attr("aria-selected","false"),o.length&&r.length?i.oldTab.attr("tabIndex",-1):o.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),o.attr({"aria-expanded":"true","aria-hidden":"false"}),i.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(t){return"string"==typeof t&&(t=this.anchors.index(this.anchors.filter("[href$='"+t+"']"))),t},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var s=this.options.disabled;s!==!1&&(i===e?s=!1:(i=this._getIndex(i),s=t.isArray(s)?t.map(s,function(t){return t!==i?t:null}):t.map(this.tabs,function(t,e){return e!==i?e:null})),this._setupDisabled(s))},disable:function(i){var s=this.options.disabled;if(s!==!0){if(i===e)s=!0;else{if(i=this._getIndex(i),-1!==t.inArray(i,s))return;s=t.isArray(s)?t.merge([i],s).sort():[i]}this._setupDisabled(s)}},load:function(e,i){e=this._getIndex(e);var n=this,a=this.tabs.eq(e),o=a.find(".ui-tabs-anchor"),r=this._getPanelForTab(a),h={tab:a,panel:r};s(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,h)),this.xhr&&"canceled"!==this.xhr.statusText&&(a.addClass("ui-tabs-loading"),r.attr("aria-busy","true"),this.xhr.success(function(t){setTimeout(function(){r.html(t),n._trigger("load",i,h)},1)}).complete(function(t,e){setTimeout(function(){"abort"===e&&n.panels.stop(!1,!0),a.removeClass("ui-tabs-loading"),r.removeAttr("aria-busy"),t===n.xhr&&delete n.xhr},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href"),beforeSend:function(e,a){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:a},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}})})(jQuery);(function(t){function e(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))}function i(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")}var s=0;t.widget("ui.tooltip",{version:"1.10.4",options:{content:function(){var e=t(this).attr("title")||"";return t("<a>").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(e,i){var s=this;return"disabled"===e?(this[i?"_disable":"_enable"](),this.options[e]=i,void 0):(this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e)}),void 0)},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0)}),this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.is("[title]")&&e.data("ui-tooltip-title",e.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).addBack().each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))})},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,o=e?e.type:null;return"string"==typeof s?this._open(e,t,s):(i=s.call(t[0],function(i){t.data("ui-tooltip-open")&&n._delay(function(){e&&(e.type=o),this._open(e,t,i)})}),i&&this._open(e,t,i),void 0)},_open:function(i,s,n){function o(t){l.of=t,a.is(":hidden")||a.position(l)}var a,r,h,l=t.extend({},this.options.position);if(n){if(a=this._find(s),a.length)return a.find(".ui-tooltip-content").html(n),void 0;s.is("[title]")&&(i&&"mouseover"===i.type?s.attr("title",""):s.removeAttr("title")),a=this._tooltip(s),e(s,a.attr("id")),a.find(".ui-tooltip-content").html(n),this.options.track&&i&&/^mouse/.test(i.type)?(this._on(this.document,{mousemove:o}),o(i)):a.position(t.extend({of:s},this.options.position)),a.hide(),this._show(a,this.options.show),this.options.show&&this.options.show.delay&&(h=this.delayedShow=setInterval(function(){a.is(":visible")&&(o(l.of),clearInterval(h))},t.fx.interval)),this._trigger("open",i,{tooltip:a}),r={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var i=t.Event(e);i.currentTarget=s[0],this.close(i,!0)}},remove:function(){this._removeTooltip(a)}},i&&"mouseover"!==i.type||(r.mouseleave="close"),i&&"focusin"!==i.type||(r.focusout="close"),this._on(!0,s,r)}},close:function(e){var s=this,n=t(e?e.currentTarget:this.element),o=this._find(n);this.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&n.attr("title",n.data("ui-tooltip-title")),i(n),o.stop(!0),this._hide(o,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),this.closing=!0,this._trigger("close",e,{tooltip:o}),this.closing=!1)},_tooltip:function(e){var i="ui-tooltip-"+s++,n=t("<div>").attr({id:i,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return t("<div>").addClass("ui-tooltip-content").appendTo(n),n.appendTo(this.document[0].body),this.tooltips[i]=e,n},_find:function(e){var i=e.data("ui-tooltip-id");return i?t("#"+i):t()},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s[0],e.close(n,!0),t("#"+i).remove(),s.data("ui-tooltip-title")&&(s.attr("title",s.data("ui-tooltip-title")),s.removeData("ui-tooltip-title"))})}})})(jQuery);
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.js	(revision 16045)
@@ -0,0 +1,12099 @@
+/**
+ * @summary     DataTables
+ * @description Paginate, search and sort HTML tables
+ * @version     1.9.4
+ * @file        jquery.dataTables.js
+ * @author      Allan Jardine (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ *
+ * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file 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 license files for details.
+ * 
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $, jQuery,define,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnDetectHeader,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap,_fnGetRowData,_fnGetCellData,_fnSetCellData,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnApplyColumnDefs,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnJsonString,_fnRender,_fnNodeToColumnIndex,_fnInfoMacros,_fnBrowserDetect,_fnGetColumns*/
+
+(/** @lends <global> */function( window, document, undefined ) {
+
+(function( factory ) {
+	"use strict";
+
+	// Define as an AMD module if possible
+	if ( typeof define === 'function' && define.amd )
+	{
+		define( ['jquery'], factory );
+	}
+	/* Define using browser globals otherwise
+	 * Prevent multiple instantiations if the script is loaded twice
+	 */
+	else if ( jQuery && !jQuery.fn.dataTable )
+	{
+		factory( jQuery );
+	}
+}
+(/** @lends <global> */function( $ ) {
+	"use strict";
+	/** 
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a 
+	 * highly flexible tool, based upon the foundations of progressive 
+	 * enhancement, which will add advanced interaction controls to any 
+	 * HTML table. For a full list of features please refer to
+	 * <a href="http://datatables.net">DataTables.net</a>.
+	 *
+	 * Note that the <i>DataTable</i> object is not a global variable but is
+	 * aliased to <i>jQuery.fn.DataTable</i> and <i>jQuery.fn.dataTable</i> through which 
+	 * it may be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [oInit={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.3+
+	 * 
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *  
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "bPaginate": false,
+	 *        "bSort": false 
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function( oInit )
+	{
+		
+		
+		/**
+		 * Add a column to the list used for the table with default values
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nTh The th element for this column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddColumn( oSettings, nTh )
+		{
+			var oDefaults = DataTable.defaults.columns;
+			var iCol = oSettings.aoColumns.length;
+			var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+				"sSortingClass": oSettings.oClasses.sSortable,
+				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
+				"nTh": nTh ? nTh : document.createElement('th'),
+				"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+				"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+				"mData": oDefaults.mData ? oDefaults.oDefaults : iCol
+			} );
+			oSettings.aoColumns.push( oCol );
+			
+			/* Add a column specific filter */
+			if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null )
+			{
+				oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch );
+			}
+			else
+			{
+				var oPre = oSettings.aoPreSearchCols[ iCol ];
+				
+				/* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */
+				if ( oPre.bRegex === undefined )
+				{
+					oPre.bRegex = true;
+				}
+				
+				if ( oPre.bSmart === undefined )
+				{
+					oPre.bSmart = true;
+				}
+				
+				if ( oPre.bCaseInsensitive === undefined )
+				{
+					oPre.bCaseInsensitive = true;
+				}
+			}
+			
+			/* Use the column options function to initialise classes etc */
+			_fnColumnOptions( oSettings, iCol, null );
+		}
+		
+		
+		/**
+		 * Apply options for a column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column index to consider
+		 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOptions( oSettings, iCol, oOptions )
+		{
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* User specified column options */
+			if ( oOptions !== undefined && oOptions !== null )
+			{
+				/* Backwards compatibility for mDataProp */
+				if ( oOptions.mDataProp && !oOptions.mData )
+				{
+					oOptions.mData = oOptions.mDataProp;
+				}
+		
+				if ( oOptions.sType !== undefined )
+				{
+					oCol.sType = oOptions.sType;
+					oCol._bAutoType = false;
+				}
+				
+				$.extend( oCol, oOptions );
+				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+		
+				/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+				 * priority if defined
+				 */
+				if ( oOptions.iDataSort !== undefined )
+				{
+					oCol.aDataSort = [ oOptions.iDataSort ];
+				}
+				_fnMap( oCol, oOptions, "aDataSort" );
+			}
+		
+			/* Cache the data get and set functions for speed */
+			var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+			var mData = _fnGetObjectDataFn( oCol.mData );
+		
+			oCol.fnGetData = function (oData, sSpecific) {
+				var innerData = mData( oData, sSpecific );
+		
+				if ( oCol.mRender && (sSpecific && sSpecific !== '') )
+				{
+					return mRender( innerData, sSpecific, oData );
+				}
+				return innerData;
+			};
+			oCol.fnSetData = _fnSetObjectDataFn( oCol.mData );
+			
+			/* Feature sorting overrides column specific when off */
+			if ( !oSettings.oFeatures.bSort )
+			{
+				oCol.bSortable = false;
+			}
+			
+			/* Check that the class assignment is correct for sorting */
+			if ( !oCol.bSortable ||
+				 ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableNone;
+				oCol.sSortingClassJUI = "";
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortable;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
+			}
+		}
+		
+		
+		/**
+		 * Adjust the table column widths for new data. Note: you would probably want to 
+		 * do a redraw after calling this function!
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAdjustColumnSizing ( oSettings )
+		{
+			/* Not interested in doing column width calculation if auto-width is disabled */
+			if ( oSettings.oFeatures.bAutoWidth === false )
+			{
+				return false;
+			}
+			
+			_fnCalculateColumnWidths( oSettings );
+			for ( var i=0 , iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;
+			}
+		}
+		
+		
+		/**
+		 * Covert the index of a visible column to the index in the data array (take account
+		 * of hidden columns)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMatch Visible column index to lookup
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisibleToColumnIndex( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		
+			return typeof aiVis[iMatch] === 'number' ?
+				aiVis[iMatch] :
+				null;
+		}
+		
+		
+		/**
+		 * Covert the index of an index in the data array and convert it to the visible
+		 *   column index (take account of hidden columns)
+		 *  @param {int} iMatch Column index to lookup
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnIndexToVisible( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+			var iPos = $.inArray( iMatch, aiVis );
+		
+			return iPos !== -1 ? iPos : null;
+		}
+		
+		
+		/**
+		 * Get the number of visible columns
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the number of visible columns
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisbleColumns( oSettings )
+		{
+			return _fnGetColumns( oSettings, 'bVisible' ).length;
+		}
+		
+		
+		/**
+		 * Get an array of column indexes that match a given property
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sParam Parameter in aoColumns to look for - typically 
+		 *    bVisible or bSearchable
+		 *  @returns {array} Array of indexes with matched properties
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetColumns( oSettings, sParam )
+		{
+			var a = [];
+		
+			$.map( oSettings.aoColumns, function(val, i) {
+				if ( val[sParam] ) {
+					a.push( i );
+				}
+			} );
+		
+			return a;
+		}
+		
+		
+		/**
+		 * Get the sort type based on an input string
+		 *  @param {string} sData data we wish to know the type of
+		 *  @returns {string} type (defaults to 'string' if no type can be detected)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectType( sData )
+		{
+			var aTypes = DataTable.ext.aTypes;
+			var iLen = aTypes.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				var sType = aTypes[i]( sData );
+				if ( sType !== null )
+				{
+					return sType;
+				}
+			}
+			
+			return 'string';
+		}
+		
+		
+		/**
+		 * Figure out how to reorder a display list
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {int} aiReturn index list for reordering
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReOrderIndex ( oSettings, sColumns )
+		{
+			var aColumns = sColumns.split(',');
+			var aiReturn = [];
+			
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				for ( var j=0 ; j<iLen ; j++ )
+				{
+					if ( oSettings.aoColumns[i].sName == aColumns[j] )
+					{
+						aiReturn.push( j );
+						break;
+					}
+				}
+			}
+			
+			return aiReturn;
+		}
+		
+		
+		/**
+		 * Get the column ordering that DataTables expects
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {string} comma separated list of names
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOrdering ( oSettings )
+		{
+			var sNames = '';
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				sNames += oSettings.aoColumns[i].sName+',';
+			}
+			if ( sNames.length == iLen )
+			{
+				return "";
+			}
+			return sNames.slice(0, -1);
+		}
+		
+		
+		/**
+		 * Take the column definitions and static columns arrays and calculate how
+		 * they relate to column indexes. The callback function will then apply the
+		 * definition found for a column to a suitable configuration object.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+		 *  @param {array} aoCols The aoColumns array that defines columns individually
+		 *  @param {function} fn Callback function - takes two parameters, the calculated
+		 *    column index and the definition for that column.
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+		{
+			var i, iLen, j, jLen, k, kLen;
+		
+			// Column definitions with aTargets
+			if ( aoColDefs )
+			{
+				/* Loop over the definitions array - loop in reverse so first instance has priority */
+				for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+				{
+					/* Each definition can target multiple columns, as it is an array */
+					var aTargets = aoColDefs[i].aTargets;
+					if ( !$.isArray( aTargets ) )
+					{
+						_fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );
+					}
+		
+					for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+					{
+						if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+						{
+							/* Add columns that we don't yet know about */
+							while( oSettings.aoColumns.length <= aTargets[j] )
+							{
+								_fnAddColumn( oSettings );
+							}
+		
+							/* Integer, basic index */
+							fn( aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+						{
+							/* Negative integer, right to left column counting */
+							fn( oSettings.aoColumns.length+aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'string' )
+						{
+							/* Class name matching on TH element */
+							for ( k=0, kLen=oSettings.aoColumns.length ; k<kLen ; k++ )
+							{
+								if ( aTargets[j] == "_all" ||
+								     $(oSettings.aoColumns[k].nTh).hasClass( aTargets[j] ) )
+								{
+									fn( k, aoColDefs[i] );
+								}
+							}
+						}
+					}
+				}
+			}
+		
+			// Statically defined columns array
+			if ( aoCols )
+			{
+				for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+				{
+					fn( i, aoCols[i] );
+				}
+			}
+		}
+		
+		/**
+		 * Add a data array to the table, creating DOM node etc. This is the parallel to 
+		 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+		 * DOM source.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData data array to be added
+		 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddData ( oSettings, aDataSupplied )
+		{
+			var oCol;
+			
+			/* Take an independent copy of the data source so we can bash it about as we wish */
+			var aDataIn = ($.isArray(aDataSupplied)) ?
+				aDataSupplied.slice() :
+				$.extend( true, {}, aDataSupplied );
+			
+			/* Create the object for storing information about this new row */
+			var iRow = oSettings.aoData.length;
+			var oData = $.extend( true, {}, DataTable.models.oRow );
+			oData._aData = aDataIn;
+			oSettings.aoData.push( oData );
+		
+			/* Create the cells */
+			var nTd, sThisType;
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+		
+				/* Use rendered data for filtering / sorting */
+				if ( typeof oCol.fnRender === 'function' && oCol.bUseRendered && oCol.mData !== null )
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnRender(oSettings, iRow, i) );
+				}
+				else
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnGetCellData( oSettings, iRow, i ) );
+				}
+				
+				/* See if we should auto-detect the column type */
+				if ( oCol._bAutoType && oCol.sType != 'string' )
+				{
+					/* Attempt to auto detect the type - same as _fnGatherData() */
+					var sVarType = _fnGetCellData( oSettings, iRow, i, 'type' );
+					if ( sVarType !== null && sVarType !== '' )
+					{
+						sThisType = _fnDetectType( sVarType );
+						if ( oCol.sType === null )
+						{
+							oCol.sType = sThisType;
+						}
+						else if ( oCol.sType != sThisType && oCol.sType != "html" )
+						{
+							/* String is always the 'fallback' option */
+							oCol.sType = 'string';
+						}
+					}
+				}
+			}
+			
+			/* Add to the display array */
+			oSettings.aiDisplayMaster.push( iRow );
+		
+			/* Create the DOM information */
+			if ( !oSettings.oFeatures.bDeferRender )
+			{
+				_fnCreateTr( oSettings, iRow );
+			}
+		
+			return iRow;
+		}
+		
+		
+		/**
+		 * Read in the data from the target table from the DOM
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGatherData( oSettings )
+		{
+			var iLoop, i, iLen, j, jLen, jInner,
+			 	nTds, nTrs, nTd, nTr, aLocalData, iThisIndex,
+				iRow, iRows, iColumn, iColumns, sNodeName,
+				oCol, oData;
+			
+			/*
+			 * Process by row first
+			 * Add the data object for the whole table - storing the tr node. Note - no point in getting
+			 * DOM based data if we are going to go and replace it with Ajax source data.
+			 */
+			if ( oSettings.bDeferLoading || oSettings.sAjaxSource === null )
+			{
+				nTr = oSettings.nTBody.firstChild;
+				while ( nTr )
+				{
+					if ( nTr.nodeName.toUpperCase() == "TR" )
+					{
+						iThisIndex = oSettings.aoData.length;
+						nTr._DT_RowIndex = iThisIndex;
+						oSettings.aoData.push( $.extend( true, {}, DataTable.models.oRow, {
+							"nTr": nTr
+						} ) );
+		
+						oSettings.aiDisplayMaster.push( iThisIndex );
+						nTd = nTr.firstChild;
+						jInner = 0;
+						while ( nTd )
+						{
+							sNodeName = nTd.nodeName.toUpperCase();
+							if ( sNodeName == "TD" || sNodeName == "TH" )
+							{
+								_fnSetCellData( oSettings, iThisIndex, jInner, $.trim(nTd.innerHTML) );
+								jInner++;
+							}
+							nTd = nTd.nextSibling;
+						}
+					}
+					nTr = nTr.nextSibling;
+				}
+			}
+			
+			/* Gather in the TD elements of the Table - note that this is basically the same as
+			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet
+			 * setup!
+			 */
+			nTrs = _fnGetTrNodes( oSettings );
+			nTds = [];
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTd = nTrs[i].firstChild;
+				while ( nTd )
+				{
+					sNodeName = nTd.nodeName.toUpperCase();
+					if ( sNodeName == "TD" || sNodeName == "TH" )
+					{
+						nTds.push( nTd );
+					}
+					nTd = nTd.nextSibling;
+				}
+			}
+			
+			/* Now process by column */
+			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+			{
+				oCol = oSettings.aoColumns[iColumn];
+		
+				/* Get the title of the column - unless there is a user set one */
+				if ( oCol.sTitle === null )
+				{
+					oCol.sTitle = oCol.nTh.innerHTML;
+				}
+				
+				var
+					bAutoType = oCol._bAutoType,
+					bRender = typeof oCol.fnRender === 'function',
+					bClass = oCol.sClass !== null,
+					bVisible = oCol.bVisible,
+					nCell, sThisType, sRendered, sValType;
+				
+				/* A single loop to rule them all (and be more efficient) */
+				if ( bAutoType || bRender || bClass || !bVisible )
+				{
+					for ( iRow=0, iRows=oSettings.aoData.length ; iRow<iRows ; iRow++ )
+					{
+						oData = oSettings.aoData[iRow];
+						nCell = nTds[ (iRow*iColumns) + iColumn ];
+						
+						/* Type detection */
+						if ( bAutoType && oCol.sType != 'string' )
+						{
+							sValType = _fnGetCellData( oSettings, iRow, iColumn, 'type' );
+							if ( sValType !== '' )
+							{
+								sThisType = _fnDetectType( sValType );
+								if ( oCol.sType === null )
+								{
+									oCol.sType = sThisType;
+								}
+								else if ( oCol.sType != sThisType && 
+								          oCol.sType != "html" )
+								{
+									/* String is always the 'fallback' option */
+									oCol.sType = 'string';
+								}
+							}
+						}
+		
+						if ( oCol.mRender )
+						{
+							// mRender has been defined, so we need to get the value and set it
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						else if ( oCol.mData !== iColumn )
+						{
+							// If mData is not the same as the column number, then we need to
+							// get the dev set value. If it is the column, no point in wasting
+							// time setting the value that is already there!
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						
+						/* Rendering */
+						if ( bRender )
+						{
+							sRendered = _fnRender( oSettings, iRow, iColumn );
+							nCell.innerHTML = sRendered;
+							if ( oCol.bUseRendered )
+							{
+								/* Use the rendered data for filtering / sorting */
+								_fnSetCellData( oSettings, iRow, iColumn, sRendered );
+							}
+						}
+						
+						/* Classes */
+						if ( bClass )
+						{
+							nCell.className += ' '+oCol.sClass;
+						}
+						
+						/* Column visibility */
+						if ( !bVisible )
+						{
+							oData._anHidden[iColumn] = nCell;
+							nCell.parentNode.removeChild( nCell );
+						}
+						else
+						{
+							oData._anHidden[iColumn] = null;
+						}
+		
+						if ( oCol.fnCreatedCell )
+						{
+							oCol.fnCreatedCell.call( oSettings.oInstance,
+								nCell, _fnGetCellData( oSettings, iRow, iColumn, 'display' ), oData._aData, iRow, iColumn
+							);
+						}
+					}
+				}
+			}
+		
+			/* Row created callbacks */
+			if ( oSettings.aoRowCreatedCallback.length !== 0 )
+			{
+				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+				{
+					oData = oSettings.aoData[i];
+					_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, i] );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Take a TR element and convert it to an index in aoData
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n the TR element to find
+		 *  @returns {int} index if the node is found, null if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToDataIndex( oSettings, n )
+		{
+			return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+		}
+		
+		
+		/**
+		 * Take a TD element and convert it into a column data index (not the visible index)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow The row number the TD/TH can be found in
+		 *  @param {node} n The TD/TH element to find
+		 *  @returns {int} index if the node is found, -1 if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToColumnIndex( oSettings, iRow, n )
+		{
+			var anCells = _fnGetTdNodes( oSettings, iRow );
+		
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( anCells[i] === n )
+				{
+					return i;
+				}
+			}
+			return -1;
+		}
+		
+		
+		/**
+		 * Get an array of data for a given row from the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {string} sSpecific data get type ('type' 'filter' 'sort')
+		 *  @param {array} aiColumns Array of column indexes to get data from
+		 *  @returns {array} Data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetRowData( oSettings, iRow, sSpecific, aiColumns )
+		{
+			var out = [];
+			for ( var i=0, iLen=aiColumns.length ; i<iLen ; i++ )
+			{
+				out.push( _fnGetCellData( oSettings, iRow, aiColumns[i], sSpecific ) );
+			}
+			return out;
+		}
+		
+		
+		/**
+		 * Get the data for a given cell from the internal cache, taking into account data mapping
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {string} sSpecific data get type ('display', 'type' 'filter' 'sort')
+		 *  @returns {*} Cell data
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetCellData( oSettings, iRow, iCol, sSpecific )
+		{
+			var sData;
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			if ( (sData=oCol.fnGetData( oData, sSpecific )) === undefined )
+			{
+				if ( oSettings.iDrawError != oSettings.iDraw && oCol.sDefaultContent === null )
+				{
+					_fnLog( oSettings, 0, "Requested unknown parameter "+
+						(typeof oCol.mData=='function' ? '{mData function}' : "'"+oCol.mData+"'")+
+						" from the data source for row "+iRow );
+					oSettings.iDrawError = oSettings.iDraw;
+				}
+				return oCol.sDefaultContent;
+			}
+		
+			/* When the data source is null, we can use default column data */
+			if ( sData === null && oCol.sDefaultContent !== null )
+			{
+				sData = oCol.sDefaultContent;
+			}
+			else if ( typeof sData === 'function' )
+			{
+				/* If the data source is a function, then we run it and use the return */
+				return sData();
+			}
+		
+			if ( sSpecific == 'display' && sData === null )
+			{
+				return '';
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * Set the value for a specific cell, into the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {*} val Value to set
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetCellData( oSettings, iRow, iCol, val )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			oCol.fnSetData( oData, val );
+		}
+		
+		
+		// Private variable that is used to match array syntax in the data property object
+		var __reArray = /\[.*?\]$/;
+		
+		/**
+		 * Return a function that can be used to get data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data get function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Give an empty string for rendering / sorting etc */
+				return function (data, type) {
+					return null;
+				};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, type, extra) {
+					return mSource( data, type, extra );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* If there is a . in the source string then the data source is in a 
+				 * nested object so we loop over the data for each level to get the next
+				 * level down. On each loop we test for undefined, and if found immediately
+				 * return. This allows entire objects to be missing and sDefaultContent to
+				 * be used if defined, rather than throwing an error
+				 */
+				var fetchData = function (data, type, src) {
+					var a = src.split('.');
+					var arrayNotation, out, innerSrc;
+		
+					if ( src !== "" )
+					{
+						for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+						{
+							// Check if we are dealing with an array notation request
+							arrayNotation = a[i].match(__reArray);
+		
+							if ( arrayNotation ) {
+								a[i] = a[i].replace(__reArray, '');
+		
+								// Condition allows simply [] to be passed in
+								if ( a[i] !== "" ) {
+									data = data[ a[i] ];
+								}
+								out = [];
+								
+								// Get the remainder of the nested object to get
+								a.splice( 0, i+1 );
+								innerSrc = a.join('.');
+		
+								// Traverse each entry in the array getting the properties requested
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+		
+								// If a string is given in between the array notation indicators, that
+								// is used to join the strings together, otherwise an array is returned
+								var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+								data = (join==="") ? out : out.join(join);
+		
+								// The inner call to fetchData has already traversed through the remainder
+								// of the source requested, so we exit from the loop
+								break;
+							}
+		
+							if ( data === null || data[ a[i] ] === undefined )
+							{
+								return undefined;
+							}
+							data = data[ a[i] ];
+						}
+					}
+		
+					return data;
+				};
+		
+				return function (data, type) {
+					return fetchData( data, type, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, type) {
+					return data[mSource];	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return a function that can be used to set data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data set function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Nothing to do when the data source is null */
+				return function (data, val) {};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, val) {
+					mSource( data, 'set', val );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* Like the get, we need to get data from a nested object */
+				var setData = function (data, val, src) {
+					var a = src.split('.'), b;
+					var arrayNotation, o, innerSrc;
+		
+					for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with an array notation request
+						arrayNotation = a[i].match(__reArray);
+		
+						if ( arrayNotation )
+						{
+							a[i] = a[i].replace(__reArray, '');
+							data[ a[i] ] = [];
+							
+							// Get the remainder of the nested object to set so we can recurse
+							b = a.slice();
+							b.splice( 0, i+1 );
+							innerSrc = b.join('.');
+		
+							// Traverse each entry in the array setting the properties requested
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+		
+							// The inner call to setData has already traversed through the remainder
+							// of the source and has set the data, thus we can exit here
+							return;
+						}
+		
+						// If the nested object doesn't currently exist - since we are
+						// trying to set the value - create it
+						if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+						{
+							data[ a[i] ] = {};
+						}
+						data = data[ a[i] ];
+					}
+		
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ a[a.length-1].replace(__reArray, '') ] = val;
+				};
+		
+				return function (data, val) {
+					return setData( data, val, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, val) {
+					data[mSource] = val;	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return an array with the full table data
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {array} aData Master data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetDataMaster ( oSettings )
+		{
+			var aData = [];
+			var iLen = oSettings.aoData.length;
+			for ( var i=0 ; i<iLen; i++ )
+			{
+				aData.push( oSettings.aoData[i]._aData );
+			}
+			return aData;
+		}
+		
+		
+		/**
+		 * Nuke the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnClearTable( oSettings )
+		{
+			oSettings.aoData.splice( 0, oSettings.aoData.length );
+			oSettings.aiDisplayMaster.splice( 0, oSettings.aiDisplayMaster.length );
+			oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length );
+			_fnCalculateEnd( oSettings );
+		}
+		
+		
+		 /**
+		 * Take an array of integers (index array) and remove a target integer (value - not 
+		 * the key!)
+		 *  @param {array} a Index array to target
+		 *  @param {int} iTarget value to find
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDeleteIndex( a, iTarget )
+		{
+			var iTargetIndex = -1;
+			
+			for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+			{
+				if ( a[i] == iTarget )
+				{
+					iTargetIndex = i;
+				}
+				else if ( a[i] > iTarget )
+				{
+					a[i]--;
+				}
+			}
+			
+			if ( iTargetIndex != -1 )
+			{
+				a.splice( iTargetIndex, 1 );
+			}
+		}
+		
+		
+		 /**
+		 * Call the developer defined fnRender function for a given cell (row/column) with
+		 * the required parameters and return the result.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData index for the row
+		 *  @param {int} iCol aoColumns index for the column
+		 *  @returns {*} Return of the developer's fnRender function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnRender( oSettings, iRow, iCol )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+		
+			return oCol.fnRender( {
+				"iDataRow":    iRow,
+				"iDataColumn": iCol,
+				"oSettings":   oSettings,
+				"aData":       oSettings.aoData[iRow]._aData,
+				"mDataProp":   oCol.mData
+			}, _fnGetCellData(oSettings, iRow, iCol, 'display') );
+		}
+		/**
+		 * Create a new TR element (and it's TD children) for a row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow Row to consider
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateTr ( oSettings, iRow )
+		{
+			var oData = oSettings.aoData[iRow];
+			var nTd;
+		
+			if ( oData.nTr === null )
+			{
+				oData.nTr = document.createElement('tr');
+		
+				/* Use a private property on the node to allow reserve mapping from the node
+				 * to the aoData array for fast look up
+				 */
+				oData.nTr._DT_RowIndex = iRow;
+		
+				/* Special parameters can be given by the data source to be used on the row */
+				if ( oData._aData.DT_RowId )
+				{
+					oData.nTr.id = oData._aData.DT_RowId;
+				}
+		
+				if ( oData._aData.DT_RowClass )
+				{
+					oData.nTr.className = oData._aData.DT_RowClass;
+				}
+		
+				/* Process each column */
+				for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					var oCol = oSettings.aoColumns[i];
+					nTd = document.createElement( oCol.sCellType );
+		
+					/* Render if needed - if bUseRendered is true then we already have the rendered
+					 * value in the data source - so can just use that
+					 */
+					nTd.innerHTML = (typeof oCol.fnRender === 'function' && (!oCol.bUseRendered || oCol.mData === null)) ?
+						_fnRender( oSettings, iRow, i ) :
+						_fnGetCellData( oSettings, iRow, i, 'display' );
+				
+					/* Add user defined class */
+					if ( oCol.sClass !== null )
+					{
+						nTd.className = oCol.sClass;
+					}
+					
+					if ( oCol.bVisible )
+					{
+						oData.nTr.appendChild( nTd );
+						oData._anHidden[i] = null;
+					}
+					else
+					{
+						oData._anHidden[i] = nTd;
+					}
+		
+					if ( oCol.fnCreatedCell )
+					{
+						oCol.fnCreatedCell.call( oSettings.oInstance,
+							nTd, _fnGetCellData( oSettings, iRow, i, 'display' ), oData._aData, iRow, i
+						);
+					}
+				}
+		
+				_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, iRow] );
+			}
+		}
+		
+		
+		/**
+		 * Create the HTML header for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildHead( oSettings )
+		{
+			var i, nTh, iLen, j, jLen;
+			var iThs = $('th, td', oSettings.nTHead).length;
+			var iCorrector = 0;
+			var jqChildren;
+			
+			/* If there is a header in place - then use it - otherwise it's going to get nuked... */
+			if ( iThs !== 0 )
+			{
+				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.setAttribute('role', 'columnheader');
+					if ( oSettings.aoColumns[i].bSortable )
+					{
+						nTh.setAttribute('tabindex', oSettings.iTabIndex);
+						nTh.setAttribute('aria-controls', oSettings.sTableId);
+					}
+		
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					/* Set the title of the column if it is user defined (not what was auto detected) */
+					if ( oSettings.aoColumns[i].sTitle != nTh.innerHTML )
+					{
+						nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					}
+				}
+			}
+			else
+			{
+				/* We don't have a header in the DOM - so we are going to have to create one */
+				var nTr = document.createElement( "tr" );
+				
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					nTh.setAttribute('tabindex', '0');
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					nTr.appendChild( nTh );
+				}
+				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );
+				_fnDetectHeader( oSettings.aoHeader, oSettings.nTHead );
+			}
+			
+			/* ARIA role for the rows */	
+			$(oSettings.nTHead).children('tr').attr('role', 'row');
+			
+			/* Add the extra markup needed by jQuery UI's themes */
+			if ( oSettings.bJUI )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					
+					var nDiv = document.createElement('div');
+					nDiv.className = oSettings.oClasses.sSortJUIWrapper;
+					$(nTh).contents().appendTo(nDiv);
+					
+					var nSpan = document.createElement('span');
+					nSpan.className = oSettings.oClasses.sSortIcon;
+					nDiv.appendChild( nSpan );
+					nTh.appendChild( nDiv );
+				}
+			}
+			
+			if ( oSettings.oFeatures.bSort )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bSortable !== false )
+					{
+						_fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+					}
+					else
+					{
+						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );
+					}
+				}
+			}
+			
+			/* Deal with the footer - add classes if required */
+			if ( oSettings.oClasses.sFooterTH !== "" )
+			{
+				$(oSettings.nTFoot).children('tr').children('th').addClass( oSettings.oClasses.sFooterTH );
+			}
+			
+			/* Cache the footer elements */
+			if ( oSettings.nTFoot !== null )
+			{
+				var anCells = _fnGetUniqueThs( oSettings, null, oSettings.aoFooter );
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( anCells[i] )
+					{
+						oSettings.aoColumns[i].nTf = anCells[i];
+						if ( oSettings.aoColumns[i].sClass )
+						{
+							$(anCells[i]).addClass( oSettings.aoColumns[i].sClass );
+						}
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Draw the header (or footer) element based on the column visibility states. The
+		 * methodology here is to use the layout array from _fnDetectHeader, modified for
+		 * the instantaneous column visibility, to construct the new layout. The grid is
+		 * traversed over cell at a time in a rows x columns grid fashion, although each 
+		 * cell insert can cover multiple elements in the grid - which is tracks using the
+		 * aApplied array. Cell inserts in the grid will only occur where there isn't
+		 * already a cell in that position.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+		 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, 
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+		{
+			var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+			var aoLocal = [];
+			var aApplied = [];
+			var iColumns = oSettings.aoColumns.length;
+			var iRowspan, iColspan;
+		
+			if (  bIncludeHidden === undefined )
+			{
+				bIncludeHidden = false;
+			}
+		
+			/* Make a copy of the master layout array, but without the visible columns in it */
+			for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+			{
+				aoLocal[i] = aoSource[i].slice();
+				aoLocal[i].nTr = aoSource[i].nTr;
+		
+				/* Remove any columns which are currently hidden */
+				for ( j=iColumns-1 ; j>=0 ; j-- )
+				{
+					if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+					{
+						aoLocal[i].splice( j, 1 );
+					}
+				}
+		
+				/* Prep the applied array - it needs an element for each row */
+				aApplied.push( [] );
+			}
+		
+			for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+			{
+				nLocalTr = aoLocal[i].nTr;
+				
+				/* All cells are going to be replaced, so empty out the row */
+				if ( nLocalTr )
+				{
+					while( (n = nLocalTr.firstChild) )
+					{
+						nLocalTr.removeChild( n );
+					}
+				}
+		
+				for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+				{
+					iRowspan = 1;
+					iColspan = 1;
+		
+					/* Check to see if there is already a cell (row/colspan) covering our target
+					 * insert point. If there is, then there is nothing to do.
+					 */
+					if ( aApplied[i][j] === undefined )
+					{
+						nLocalTr.appendChild( aoLocal[i][j].cell );
+						aApplied[i][j] = 1;
+		
+						/* Expand the cell to cover as many rows as needed */
+						while ( aoLocal[i+iRowspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+						{
+							aApplied[i+iRowspan][j] = 1;
+							iRowspan++;
+						}
+		
+						/* Expand the cell to cover as many columns as needed */
+						while ( aoLocal[i][j+iColspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+						{
+							/* Must update the applied array over the rows for the columns */
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aApplied[i+k][j+iColspan] = 1;
+							}
+							iColspan++;
+						}
+		
+						/* Do the actual expansion in the DOM */
+						aoLocal[i][j].cell.rowSpan = iRowspan;
+						aoLocal[i][j].cell.colSpan = iColspan;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Insert the required TR nodes into the table for display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDraw( oSettings )
+		{
+			/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+			var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+			if ( $.inArray( false, aPreDraw ) !== -1 )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				return;
+			}
+			
+			var i, iLen, n;
+			var anRows = [];
+			var iRowCount = 0;
+			var iStripes = oSettings.asStripeClasses.length;
+			var iOpenRows = oSettings.aoOpenRows.length;
+			
+			oSettings.bDrawing = true;
+			
+			/* Check and see if we have an initial draw position from state saving */
+			if ( oSettings.iInitDisplayStart !== undefined && oSettings.iInitDisplayStart != -1 )
+			{
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					oSettings._iDisplayStart = oSettings.iInitDisplayStart;
+				}
+				else
+				{
+					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?
+						0 : oSettings.iInitDisplayStart;
+				}
+				oSettings.iInitDisplayStart = -1;
+				_fnCalculateEnd( oSettings );
+			}
+			
+			/* Server-side processing draw intercept */
+			if ( oSettings.bDeferLoading )
+			{
+				oSettings.bDeferLoading = false;
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.oFeatures.bServerSide )
+			{
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+			{
+				return;
+			}
+			
+			if ( oSettings.aiDisplay.length !== 0 )
+			{
+				var iStart = oSettings._iDisplayStart;
+				var iEnd = oSettings._iDisplayEnd;
+				
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					iStart = 0;
+					iEnd = oSettings.aoData.length;
+				}
+				
+				for ( var j=iStart ; j<iEnd ; j++ )
+				{
+					var aoData = oSettings.aoData[ oSettings.aiDisplay[j] ];
+					if ( aoData.nTr === null )
+					{
+						_fnCreateTr( oSettings, oSettings.aiDisplay[j] );
+					}
+		
+					var nRow = aoData.nTr;
+					
+					/* Remove the old striping classes and then add the new one */
+					if ( iStripes !== 0 )
+					{
+						var sStripe = oSettings.asStripeClasses[ iRowCount % iStripes ];
+						if ( aoData._sRowStripe != sStripe )
+						{
+							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+							aoData._sRowStripe = sStripe;
+						}
+					}
+					
+					/* Row callback functions - might want to manipulate the row */
+					_fnCallbackFire( oSettings, 'aoRowCallback', null, 
+						[nRow, oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j] );
+					
+					anRows.push( nRow );
+					iRowCount++;
+					
+					/* If there is an open row - and it is attached to this parent - attach it on redraw */
+					if ( iOpenRows !== 0 )
+					{
+						for ( var k=0 ; k<iOpenRows ; k++ )
+						{
+							if ( nRow == oSettings.aoOpenRows[k].nParent )
+							{
+								anRows.push( oSettings.aoOpenRows[k].nTr );
+								break;
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Table is empty - create a row with an empty message in it */
+				anRows[ 0 ] = document.createElement( 'tr' );
+				
+				if ( oSettings.asStripeClasses[0] )
+				{
+					anRows[ 0 ].className = oSettings.asStripeClasses[0];
+				}
+		
+				var oLang = oSettings.oLanguage;
+				var sZero = oLang.sZeroRecords;
+				if ( oSettings.iDraw == 1 && oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+				{
+					sZero = oLang.sLoadingRecords;
+				}
+				else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+				{
+					sZero = oLang.sEmptyTable;
+				}
+		
+				var nTd = document.createElement( 'td' );
+				nTd.setAttribute( 'valign', "top" );
+				nTd.colSpan = _fnVisbleColumns( oSettings );
+				nTd.className = oSettings.oClasses.sRowEmpty;
+				nTd.innerHTML = _fnInfoMacros( oSettings, sZero );
+				
+				anRows[ iRowCount ].appendChild( nTd );
+			}
+			
+			/* Header and footer callbacks */
+			_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			/* 
+			 * Need to remove any old row from the display - note we can't just empty the tbody using
+			 * $().html('') since this will unbind the jQuery event handlers (even although the node 
+			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.
+			 */
+			var
+				nAddFrag = document.createDocumentFragment(),
+				nRemoveFrag = document.createDocumentFragment(),
+				nBodyPar, nTrs;
+			
+			if ( oSettings.nTBody )
+			{
+				nBodyPar = oSettings.nTBody.parentNode;
+				nRemoveFrag.appendChild( oSettings.nTBody );
+				
+				/* When doing infinite scrolling, only remove child rows when sorting, filtering or start
+				 * up. When not infinite scroll, always do it.
+				 */
+				if ( !oSettings.oScroll.bInfinite || !oSettings._bInitComplete ||
+				 	oSettings.bSorted || oSettings.bFiltered )
+				{
+					while( (n = oSettings.nTBody.firstChild) )
+					{
+						oSettings.nTBody.removeChild( n );
+					}
+				}
+				
+				/* Put the draw table into the dom */
+				for ( i=0, iLen=anRows.length ; i<iLen ; i++ )
+				{
+					nAddFrag.appendChild( anRows[i] );
+				}
+				
+				oSettings.nTBody.appendChild( nAddFrag );
+				if ( nBodyPar !== null )
+				{
+					nBodyPar.appendChild( oSettings.nTBody );
+				}
+			}
+			
+			/* Call all required callback functions for the end of a draw */
+			_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+			
+			/* Draw is complete, sorting and filtering must be as well */
+			oSettings.bSorted = false;
+			oSettings.bFiltered = false;
+			oSettings.bDrawing = false;
+			
+			if ( oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				if ( !oSettings._bInitComplete )
+				{
+					_fnInitComplete( oSettings );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Redraw the table - taking account of the various features which are enabled
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReDraw( oSettings )
+		{
+			if ( oSettings.oFeatures.bSort )
+			{
+				/* Sorting will refilter and draw for us */
+				_fnSort( oSettings, oSettings.oPreviousSearch );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				/* Filtering will redraw for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Add the options to the page HTML for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddOptionsHtml ( oSettings )
+		{
+			/*
+			 * Create a temporary, empty, div which we can later on replace with what we have generated
+			 * we do it this way to rendering the 'options' html offline - speed :-)
+			 */
+			var nHolding = $('<div></div>')[0];
+			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
+			
+			/* 
+			 * All DataTables are wrapped in a div
+			 */
+			oSettings.nTableWrapper = $('<div id="'+oSettings.sTableId+'_wrapper" class="'+oSettings.oClasses.sWrapper+'" role="grid"></div>')[0];
+			oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+		
+			/* Track where we want to insert the option */
+			var nInsertNode = oSettings.nTableWrapper;
+			
+			/* Loop over the user set positioning and place the elements as needed */
+			var aDom = oSettings.sDom.split('');
+			var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;
+			for ( var i=0 ; i<aDom.length ; i++ )
+			{
+				iPushFeature = 0;
+				cOption = aDom[i];
+				
+				if ( cOption == '<' )
+				{
+					/* New container div */
+					nNewNode = $('<div></div>')[0];
+					
+					/* Check to see if we should append an id and/or a class name to the container */
+					cNext = aDom[i+1];
+					if ( cNext == "'" || cNext == '"' )
+					{
+						sAttr = "";
+						j = 2;
+						while ( aDom[i+j] != cNext )
+						{
+							sAttr += aDom[i+j];
+							j++;
+						}
+						
+						/* Replace jQuery UI constants */
+						if ( sAttr == "H" )
+						{
+							sAttr = oSettings.oClasses.sJUIHeader;
+						}
+						else if ( sAttr == "F" )
+						{
+							sAttr = oSettings.oClasses.sJUIFooter;
+						}
+						
+						/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+						 * breaks the string into parts and applies them as needed
+						 */
+						if ( sAttr.indexOf('.') != -1 )
+						{
+							var aSplit = sAttr.split('.');
+							nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+							nNewNode.className = aSplit[1];
+						}
+						else if ( sAttr.charAt(0) == "#" )
+						{
+							nNewNode.id = sAttr.substr(1, sAttr.length-1);
+						}
+						else
+						{
+							nNewNode.className = sAttr;
+						}
+						
+						i += j; /* Move along the position array */
+					}
+					
+					nInsertNode.appendChild( nNewNode );
+					nInsertNode = nNewNode;
+				}
+				else if ( cOption == '>' )
+				{
+					/* End container div */
+					nInsertNode = nInsertNode.parentNode;
+				}
+				else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )
+				{
+					/* Length */
+					nTmp = _fnFeatureHtmlLength( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'f' && oSettings.oFeatures.bFilter )
+				{
+					/* Filter */
+					nTmp = _fnFeatureHtmlFilter( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )
+				{
+					/* pRocessing */
+					nTmp = _fnFeatureHtmlProcessing( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 't' )
+				{
+					/* Table */
+					nTmp = _fnFeatureHtmlTable( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption ==  'i' && oSettings.oFeatures.bInfo )
+				{
+					/* Info */
+					nTmp = _fnFeatureHtmlInfo( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )
+				{
+					/* Pagination */
+					nTmp = _fnFeatureHtmlPaginate( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( DataTable.ext.aoFeatures.length !== 0 )
+				{
+					/* Plug-in features */
+					var aoFeatures = DataTable.ext.aoFeatures;
+					for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+					{
+						if ( cOption == aoFeatures[k].cFeature )
+						{
+							nTmp = aoFeatures[k].fnInit( oSettings );
+							if ( nTmp )
+							{
+								iPushFeature = 1;
+							}
+							break;
+						}
+					}
+				}
+				
+				/* Add to the 2D features array */
+				if ( iPushFeature == 1 && nTmp !== null )
+				{
+					if ( typeof oSettings.aanFeatures[cOption] !== 'object' )
+					{
+						oSettings.aanFeatures[cOption] = [];
+					}
+					oSettings.aanFeatures[cOption].push( nTmp );
+					nInsertNode.appendChild( nTmp );
+				}
+			}
+			
+			/* Built our DOM structure - replace the holding div with what we want */
+			nHolding.parentNode.replaceChild( oSettings.nTableWrapper, nHolding );
+		}
+		
+		
+		/**
+		 * Use the DOM source to create up an array of header cells. The idea here is to
+		 * create a layout grid (array) of rows x columns, which contains a reference
+		 * to the cell that that point in the grid (regardless of col/rowspan), such that
+		 * any column / row could be removed and the new grid constructed
+		 *  @param array {object} aLayout Array to store the calculated layout in
+		 *  @param {node} nThead The header/footer element for the table
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectHeader ( aLayout, nThead )
+		{
+			var nTrs = $(nThead).children('tr');
+			var nTr, nCell;
+			var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+			var bUnique;
+			var fnShiftCol = function ( a, i, j ) {
+				var k = a[i];
+		                while ( k[j] ) {
+					j++;
+				}
+				return j;
+			};
+		
+			aLayout.splice( 0, aLayout.length );
+			
+			/* We know how many rows there are in the layout - so prep it */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				aLayout.push( [] );
+			}
+			
+			/* Calculate a layout array */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTr = nTrs[i];
+				iColumn = 0;
+				
+				/* For every cell in the row... */
+				nCell = nTr.firstChild;
+				while ( nCell ) {
+					if ( nCell.nodeName.toUpperCase() == "TD" ||
+					     nCell.nodeName.toUpperCase() == "TH" )
+					{
+						/* Get the col and rowspan attributes from the DOM and sanitise them */
+						iColspan = nCell.getAttribute('colspan') * 1;
+						iRowspan = nCell.getAttribute('rowspan') * 1;
+						iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+						iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+		
+						/* There might be colspan cells already in this row, so shift our target 
+						 * accordingly
+						 */
+						iColShifted = fnShiftCol( aLayout, i, iColumn );
+						
+						/* Cache calculation for unique columns */
+						bUnique = iColspan === 1 ? true : false;
+						
+						/* If there is col / rowspan, copy the information into the layout grid */
+						for ( l=0 ; l<iColspan ; l++ )
+						{
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aLayout[i+k][iColShifted+l] = {
+									"cell": nCell,
+									"unique": bUnique
+								};
+								aLayout[i+k].nTr = nTr;
+							}
+						}
+					}
+					nCell = nCell.nextSibling;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Get an array of unique th elements, one for each column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nHeader automatically detect the layout from this node - optional
+		 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+		 *  @returns array {node} aReturn list of unique th's
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+		{
+			var aReturn = [];
+			if ( !aLayout )
+			{
+				aLayout = oSettings.aoHeader;
+				if ( nHeader )
+				{
+					aLayout = [];
+					_fnDetectHeader( aLayout, nHeader );
+				}
+			}
+		
+			for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+			{
+				for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+				{
+					if ( aLayout[i][j].unique && 
+						 (!aReturn[j] || !oSettings.bSortCellsTop) )
+					{
+						aReturn[j] = aLayout[i][j].cell;
+					}
+				}
+			}
+			
+			return aReturn;
+		}
+		
+		
+		
+		/**
+		 * Update the table using an Ajax call
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {boolean} Block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdate( oSettings )
+		{
+			if ( oSettings.bAjaxDataGet )
+			{
+				oSettings.iDraw++;
+				_fnProcessingDisplay( oSettings, true );
+				var iColumns = oSettings.aoColumns.length;
+				var aoData = _fnAjaxParameters( oSettings );
+				_fnServerParams( oSettings, aoData );
+				
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,
+					function(json) {
+						_fnAjaxUpdateDraw( oSettings, json );
+					}, oSettings );
+				return false;
+			}
+			else
+			{
+				return true;
+			}
+		}
+		
+		
+		/**
+		 * Build up the parameters in an object needed for a server-side processing request
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {bool} block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxParameters( oSettings )
+		{
+			var iColumns = oSettings.aoColumns.length;
+			var aoData = [], mDataProp, aaSort, aDataSort;
+			var i, j;
+			
+			aoData.push( { "name": "sEcho",          "value": oSettings.iDraw } );
+			aoData.push( { "name": "iColumns",       "value": iColumns } );
+			aoData.push( { "name": "sColumns",       "value": _fnColumnOrdering(oSettings) } );
+			aoData.push( { "name": "iDisplayStart",  "value": oSettings._iDisplayStart } );
+			aoData.push( { "name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?
+				oSettings._iDisplayLength : -1 } );
+				
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+			  mDataProp = oSettings.aoColumns[i].mData;
+				aoData.push( { "name": "mDataProp_"+i, "value": typeof(mDataProp)==="function" ? 'function' : mDataProp } );
+			}
+			
+			/* Filtering */
+			if ( oSettings.oFeatures.bFilter !== false )
+			{
+				aoData.push( { "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch } );
+				aoData.push( { "name": "bRegex",  "value": oSettings.oPreviousSearch.bRegex } );
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "sSearch_"+i,     "value": oSettings.aoPreSearchCols[i].sSearch } );
+					aoData.push( { "name": "bRegex_"+i,      "value": oSettings.aoPreSearchCols[i].bRegex } );
+					aoData.push( { "name": "bSearchable_"+i, "value": oSettings.aoColumns[i].bSearchable } );
+				}
+			}
+			
+			/* Sorting */
+			if ( oSettings.oFeatures.bSort !== false )
+			{
+				var iCounter = 0;
+		
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					aDataSort = oSettings.aoColumns[ aaSort[i][0] ].aDataSort;
+					
+					for ( j=0 ; j<aDataSort.length ; j++ )
+					{
+						aoData.push( { "name": "iSortCol_"+iCounter,  "value": aDataSort[j] } );
+						aoData.push( { "name": "sSortDir_"+iCounter,  "value": aaSort[i][1] } );
+						iCounter++;
+					}
+				}
+				aoData.push( { "name": "iSortingCols",   "value": iCounter } );
+				
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );
+				}
+			}
+			
+			return aoData;
+		}
+		
+		
+		/**
+		 * Add Ajax parameters from plug-ins
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoData name/value pairs to send to the server
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnServerParams( oSettings, aoData )
+		{
+			_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [aoData] );
+		}
+		
+		
+		/**
+		 * Data the data from the server (nuking the old) and redraw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} json json data return from the server.
+		 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+		 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+		 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+		 *  @param {array} json.aaData The data to display on this page
+		 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdateDraw ( oSettings, json )
+		{
+			if ( json.sEcho !== undefined )
+			{
+				/* Protect against old returns over-writing a new one. Possible when you get
+				 * very fast interaction, and later queries are completed much faster
+				 */
+				if ( json.sEcho*1 < oSettings.iDraw )
+				{
+					return;
+				}
+				else
+				{
+					oSettings.iDraw = json.sEcho * 1;
+				}
+			}
+			
+			if ( !oSettings.oScroll.bInfinite ||
+				   (oSettings.oScroll.bInfinite && (oSettings.bSorted || oSettings.bFiltered)) )
+			{
+				_fnClearTable( oSettings );
+			}
+			oSettings._iRecordsTotal = parseInt(json.iTotalRecords, 10);
+			oSettings._iRecordsDisplay = parseInt(json.iTotalDisplayRecords, 10);
+			
+			/* Determine if reordering is required */
+			var sOrdering = _fnColumnOrdering(oSettings);
+			var bReOrder = (json.sColumns !== undefined && sOrdering !== "" && json.sColumns != sOrdering );
+			var aiIndex;
+			if ( bReOrder )
+			{
+				aiIndex = _fnReOrderIndex( oSettings, json.sColumns );
+			}
+			
+			var aData = _fnGetObjectDataFn( oSettings.sAjaxDataProp )( json );
+			for ( var i=0, iLen=aData.length ; i<iLen ; i++ )
+			{
+				if ( bReOrder )
+				{
+					/* If we need to re-order, then create a new array with the correct order and add it */
+					var aDataSorted = [];
+					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+					{
+						aDataSorted.push( aData[i][ aiIndex[j] ] );
+					}
+					_fnAddData( oSettings, aDataSorted );
+				}
+				else
+				{
+					/* No re-order required, sever got it "right" - just straight add */
+					_fnAddData( oSettings, aData[i] );
+				}
+			}
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			oSettings.bAjaxDataGet = false;
+			_fnDraw( oSettings );
+			oSettings.bAjaxDataGet = true;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for filtering text
+		 *  @returns {node} Filter control element
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlFilter ( oSettings )
+		{
+			var oPreviousSearch = oSettings.oPreviousSearch;
+			
+			var sSearchStr = oSettings.oLanguage.sSearch;
+			sSearchStr = (sSearchStr.indexOf('_INPUT_') !== -1) ?
+			  sSearchStr.replace('_INPUT_', '<input type="text" />') :
+			  sSearchStr==="" ? '<input type="text" />' : sSearchStr+' <input type="text" />';
+			
+			var nFilter = document.createElement( 'div' );
+			nFilter.className = oSettings.oClasses.sFilter;
+			nFilter.innerHTML = '<label>'+sSearchStr+'</label>';
+			if ( !oSettings.aanFeatures.f )
+			{
+				nFilter.id = oSettings.sTableId+'_filter';
+			}
+			
+			var jqFilter = $('input[type="text"]', nFilter);
+		
+			// Store a reference to the input element, so other input elements could be
+			// added to the filter wrapper if needed (submit button for example)
+			nFilter._DT_Input = jqFilter[0];
+		
+			jqFilter.val( oPreviousSearch.sSearch.replace('"','&quot;') );
+			jqFilter.bind( 'keyup.DT', function(e) {
+				/* Update all other filter input elements for the new display */
+				var n = oSettings.aanFeatures.f;
+				var val = this.value==="" ? "" : this.value; // mental IE8 fix :-(
+		
+				for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != $(this).parents('div.dataTables_filter')[0] )
+					{
+						$(n[i]._DT_Input).val( val );
+					}
+				}
+				
+				/* Now do the filter */
+				if ( val != oPreviousSearch.sSearch )
+				{
+					_fnFilterComplete( oSettings, { 
+						"sSearch": val, 
+						"bRegex": oPreviousSearch.bRegex,
+						"bSmart": oPreviousSearch.bSmart ,
+						"bCaseInsensitive": oPreviousSearch.bCaseInsensitive 
+					} );
+				}
+			} );
+		
+			jqFilter
+				.attr('aria-controls', oSettings.sTableId)
+				.bind( 'keypress.DT', function(e) {
+					/* Prevent form submission */
+					if ( e.keyCode == 13 )
+					{
+						return false;
+					}
+				}
+			);
+			
+			return nFilter;
+		}
+		
+		
+		/**
+		 * Filter the table using both the global filter and column based filtering
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oSearch search information
+		 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterComplete ( oSettings, oInput, iForce )
+		{
+			var oPrevSearch = oSettings.oPreviousSearch;
+			var aoPrevSearch = oSettings.aoPreSearchCols;
+			var fnSaveFilter = function ( oFilter ) {
+				/* Save the filtering values */
+				oPrevSearch.sSearch = oFilter.sSearch;
+				oPrevSearch.bRegex = oFilter.bRegex;
+				oPrevSearch.bSmart = oFilter.bSmart;
+				oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+			};
+		
+			/* In server-side processing all filtering is done by the server, so no point hanging around here */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Global filter */
+				_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart, oInput.bCaseInsensitive );
+				fnSaveFilter( oInput );
+		
+				/* Now do the individual column filter */
+				for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+				{
+					_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, aoPrevSearch[i].bRegex, 
+						aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+				}
+				
+				/* Custom filtering */
+				_fnFilterCustom( oSettings );
+			}
+			else
+			{
+				fnSaveFilter( oInput );
+			}
+			
+			/* Tell the draw function we have been filtering */
+			oSettings.bFiltered = true;
+			$(oSettings.oInstance).trigger('filter', oSettings);
+			
+			/* Redraw the table */
+			oSettings._iDisplayStart = 0;
+			_fnCalculateEnd( oSettings );
+			_fnDraw( oSettings );
+			
+			/* Rebuild search array 'offline' */
+			_fnBuildSearchArray( oSettings, 0 );
+		}
+		
+		
+		/**
+		 * Apply custom filtering functions
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCustom( oSettings )
+		{
+			var afnFilters = DataTable.ext.afnFiltering;
+			var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+		
+			for ( var i=0, iLen=afnFilters.length ; i<iLen ; i++ )
+			{
+				var iCorrector = 0;
+				for ( var j=0, jLen=oSettings.aiDisplay.length ; j<jLen ; j++ )
+				{
+					var iDisIndex = oSettings.aiDisplay[j-iCorrector];
+					var bTest = afnFilters[i](
+						oSettings,
+						_fnGetRowData( oSettings, iDisIndex, 'filter', aiFilterColumns ),
+						iDisIndex
+					);
+					
+					/* Check if we should use this row based on the filtering function */
+					if ( !bTest )
+					{
+						oSettings.aiDisplay.splice( j-iCorrector, 1 );
+						iCorrector++;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the table on a per-column basis
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iColumn column to filter
+		 *  @param {bool} bRegex treat search string as a regular expression or not
+		 *  @param {bool} bSmart use smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart, bCaseInsensitive )
+		{
+			if ( sInput === "" )
+			{
+				return;
+			}
+			
+			var iIndexCorrector = 0;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			
+			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )
+			{
+				var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ),
+					oSettings.aoColumns[iColumn].sType );
+				if ( ! rpSearch.test( sData ) )
+				{
+					oSettings.aiDisplay.splice( i, 1 );
+					iIndexCorrector++;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the data table based on user input and draw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0)
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive )
+		{
+			var i;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			var oPrevSearch = oSettings.oPreviousSearch;
+			
+			/* Check if we are forcing or not - optional parameter */
+			if ( !iForce )
+			{
+				iForce = 0;
+			}
+			
+			/* Need to take account of custom filtering functions - always filter */
+			if ( DataTable.ext.afnFiltering.length !== 0 )
+			{
+				iForce = 1;
+			}
+			
+			/*
+			 * If the input is blank - we want the full data set
+			 */
+			if ( sInput.length <= 0 )
+			{
+				oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			}
+			else
+			{
+				/*
+				 * We are starting a new search or the new search string is smaller 
+				 * then the old one (i.e. delete). Search from the master array
+			 	 */
+				if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
+					   oPrevSearch.sSearch.length > sInput.length || iForce == 1 ||
+					   sInput.indexOf(oPrevSearch.sSearch) !== 0 )
+				{
+					/* Nuke the old display array - we are going to rebuild it */
+					oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+					
+					/* Force a rebuild of the search array */
+					_fnBuildSearchArray( oSettings, 1 );
+					
+					/* Search through all records to populate the search array
+					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 
+					 * mapping
+					 */
+					for ( i=0 ; i<oSettings.aiDisplayMaster.length ; i++ )
+					{
+						if ( rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+							oSettings.aiDisplay.push( oSettings.aiDisplayMaster[i] );
+						}
+					}
+			  }
+			  else
+				{
+			  	/* Using old search array - refine it - do it this way for speed
+			  	 * Don't have to search the whole master array again
+					 */
+			  	var iIndexCorrector = 0;
+			  	
+			  	/* Search the current results */
+			  	for ( i=0 ; i<oSettings.asDataSearch.length ; i++ )
+					{
+			  		if ( ! rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+			  			oSettings.aiDisplay.splice( i-iIndexCorrector, 1 );
+			  			iIndexCorrector++;
+			  		}
+			  	}
+			  }
+			}
+		}
+		
+		
+		/**
+		 * Create an array which can be quickly search through
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMaster use the master data array - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchArray ( oSettings, iMaster )
+		{
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Clear out the old data */
+				oSettings.asDataSearch = [];
+		
+				var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+				var aiIndex = (iMaster===1) ?
+				 	oSettings.aiDisplayMaster :
+				 	oSettings.aiDisplay;
+				
+				for ( var i=0, iLen=aiIndex.length ; i<iLen ; i++ )
+				{
+					oSettings.asDataSearch[i] = _fnBuildSearchRow(
+						oSettings,
+						_fnGetRowData( oSettings, aiIndex[i], 'filter', aiFilterColumns )
+					);
+				}
+			}
+		}
+		
+		
+		/**
+		 * Create a searchable string from a single data row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData Row data array to use for the data to search
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchRow( oSettings, aData )
+		{
+			var sSearch = aData.join('  ');
+			
+			/* If it looks like there is an HTML entity in the string, attempt to decode it */
+			if ( sSearch.indexOf('&') !== -1 )
+			{
+				sSearch = $('<div>').html(sSearch).text();
+			}
+			
+			// Strip newline characters
+			return sSearch.replace( /[\n\r]/g, " " );
+		}
+		
+		/**
+		 * Build a regular expression object suitable for searching a table
+		 *  @param {string} sSearch string to search for
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+		 *  @returns {RegExp} constructed object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive )
+		{
+			var asSearch, sRegExpString;
+			
+			if ( bSmart )
+			{
+				/* Generate the regular expression to use. Something along the lines of:
+				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
+				 */
+				asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );
+				sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';
+				return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" );
+			}
+			else
+			{
+				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
+				return new RegExp( sSearch, bCaseInsensitive ? "i" : "" );
+			}
+		}
+		
+		
+		/**
+		 * Convert raw data into something that the user can search on
+		 *  @param {string} sData data to be modified
+		 *  @param {string} sType data type
+		 *  @returns {string} search string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDataToSearch ( sData, sType )
+		{
+			if ( typeof DataTable.ext.ofnSearch[sType] === "function" )
+			{
+				return DataTable.ext.ofnSearch[sType]( sData );
+			}
+			else if ( sData === null )
+			{
+				return '';
+			}
+			else if ( sType == "html" )
+			{
+				return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" );
+			}
+			else if ( typeof sData === "string" )
+			{
+				return sData.replace(/[\r\n]/g," ");
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * scape a string such that it can be used in a regular expression
+		 *  @param {string} sVal string to escape
+		 *  @returns {string} escaped string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnEscapeRegex ( sVal )
+		{
+			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ];
+			var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
+			return sVal.replace(reReplace, '\\$1');
+		}
+		
+		
+		/**
+		 * Generate the node required for the info display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Information element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlInfo ( oSettings )
+		{
+			var nInfo = document.createElement( 'div' );
+			nInfo.className = oSettings.oClasses.sInfo;
+			
+			/* Actions that are to be taken once only for this feature */
+			if ( !oSettings.aanFeatures.i )
+			{
+				/* Add draw callback */
+				oSettings.aoDrawCallback.push( {
+					"fn": _fnUpdateInfo,
+					"sName": "information"
+				} );
+				
+				/* Add id */
+				nInfo.id = oSettings.sTableId+'_info';
+			}
+			oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' );
+			
+			return nInfo;
+		}
+		
+		
+		/**
+		 * Update the information elements in the display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnUpdateInfo ( oSettings )
+		{
+			/* Show information about the table */
+			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
+			{
+				return;
+			}
+			
+			var
+				oLang = oSettings.oLanguage,
+				iStart = oSettings._iDisplayStart+1,
+				iEnd = oSettings.fnDisplayEnd(),
+				iMax = oSettings.fnRecordsTotal(),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sOut;
+			
+			if ( iTotal === 0 )
+			{
+				/* Empty record set */
+				sOut = oLang.sInfoEmpty;
+			}
+			else {
+				/* Normal record set */
+				sOut = oLang.sInfo;
+			}
+		
+			if ( iTotal != iMax )
+			{
+				/* Record set after filtering */
+				sOut += ' ' + oLang.sInfoFiltered;
+			}
+		
+			// Convert the macros
+			sOut += oLang.sInfoPostFix;
+			sOut = _fnInfoMacros( oSettings, sOut );
+			
+			if ( oLang.fnInfoCallback !== null )
+			{
+				sOut = oLang.fnInfoCallback.call( oSettings.oInstance, 
+					oSettings, iStart, iEnd, iMax, iTotal, sOut );
+			}
+			
+			var n = oSettings.aanFeatures.i;
+			for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+			{
+				$(n[i]).html( sOut );
+			}
+		}
+		
+		
+		function _fnInfoMacros ( oSettings, str )
+		{
+			var
+				iStart = oSettings._iDisplayStart+1,
+				sStart = oSettings.fnFormatNumber( iStart ),
+				iEnd = oSettings.fnDisplayEnd(),
+				sEnd = oSettings.fnFormatNumber( iEnd ),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sTotal = oSettings.fnFormatNumber( iTotal ),
+				iMax = oSettings.fnRecordsTotal(),
+				sMax = oSettings.fnFormatNumber( iMax );
+		
+			// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+			// internally
+			if ( oSettings.oScroll.bInfinite )
+			{
+				sStart = oSettings.fnFormatNumber( 1 );
+			}
+		
+			return str.
+				replace(/_START_/g, sStart).
+				replace(/_END_/g,   sEnd).
+				replace(/_TOTAL_/g, sTotal).
+				replace(/_MAX_/g,   sMax);
+		}
+		
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitialise ( oSettings )
+		{
+			var i, iLen, iAjaxStart=oSettings.iInitDisplayStart;
+			
+			/* Ensure that the table data is fully initialised */
+			if ( oSettings.bInitialised === false )
+			{
+				setTimeout( function(){ _fnInitialise( oSettings ); }, 200 );
+				return;
+			}
+			
+			/* Show the display HTML options */
+			_fnAddOptionsHtml( oSettings );
+			
+			/* Build and draw the header / footer for the table */
+			_fnBuildHead( oSettings );
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+		
+			/* Okay to show that something is going on now */
+			_fnProcessingDisplay( oSettings, true );
+			
+			/* Calculate sizes for columns */
+			if ( oSettings.oFeatures.bAutoWidth )
+			{
+				_fnCalculateColumnWidths( oSettings );
+			}
+			
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoColumns[i].sWidth !== null )
+				{
+					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );
+				}
+			}
+			
+			/* If there is default sorting required - let's do it. The sort function will do the
+			 * drawing for us. Otherwise we draw the table regardless of the Ajax source - this allows
+			 * the table to look initialised for Ajax sourcing data (show 'loading' message possibly)
+			 */
+			if ( oSettings.oFeatures.bSort )
+			{
+				_fnSort( oSettings );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			/* if there is an ajax source load the data */
+			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+			{
+				var aoData = [];
+				_fnServerParams( oSettings, aoData );
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData, function(json) {
+					var aData = (oSettings.sAjaxDataProp !== "") ?
+					 	_fnGetObjectDataFn( oSettings.sAjaxDataProp )(json) : json;
+		
+					/* Got the data - add it to the table */
+					for ( i=0 ; i<aData.length ; i++ )
+					{
+						_fnAddData( oSettings, aData[i] );
+					}
+					
+					/* Reset the init display for cookie saving. We've already done a filter, and
+					 * therefore cleared it before. So we need to make it appear 'fresh'
+					 */
+					oSettings.iInitDisplayStart = iAjaxStart;
+					
+					if ( oSettings.oFeatures.bSort )
+					{
+						_fnSort( oSettings );
+					}
+					else
+					{
+						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+						_fnCalculateEnd( oSettings );
+						_fnDraw( oSettings );
+					}
+					
+					_fnProcessingDisplay( oSettings, false );
+					_fnInitComplete( oSettings, json );
+				}, oSettings );
+				return;
+			}
+			
+			/* Server-side processing initialisation complete is done at the end of _fnDraw */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				_fnInitComplete( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+		 *    with client-side processing (optional)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitComplete ( oSettings, json )
+		{
+			oSettings._bInitComplete = true;
+			_fnCallbackFire( oSettings, 'aoInitComplete', 'init', [oSettings, json] );
+		}
+		
+		
+		/**
+		 * Language compatibility - when certain options are given, and others aren't, we
+		 * need to duplicate the values over, in order to provide backwards compatibility
+		 * with older language files.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLanguageCompat( oLanguage )
+		{
+			var oDefaults = DataTable.defaults.oLanguage;
+		
+			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+			 * sZeroRecords - assuming that is given.
+			 */
+			if ( !oLanguage.sEmptyTable && oLanguage.sZeroRecords &&
+				oDefaults.sEmptyTable === "No data available in table" )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
+			}
+		
+			/* Likewise with loading records */
+			if ( !oLanguage.sLoadingRecords && oLanguage.sZeroRecords &&
+				oDefaults.sLoadingRecords === "Loading..." )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sLoadingRecords' );
+			}
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for user display length changing
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Display length feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlLength ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			/* This can be overruled by not using the _MENU_ var/macro in the language variable */
+			var sName = 'name="'+oSettings.sTableId+'_length"';
+			var sStdMenu = '<select size="1" '+sName+'>';
+			var i, iLen;
+			var aLengthMenu = oSettings.aLengthMenu;
+			
+			if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' && 
+					typeof aLengthMenu[1] === 'object' )
+			{
+				for ( i=0, iLen=aLengthMenu[0].length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[0][i]+'">'+aLengthMenu[1][i]+'</option>';
+				}
+			}
+			else
+			{
+				for ( i=0, iLen=aLengthMenu.length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[i]+'">'+aLengthMenu[i]+'</option>';
+				}
+			}
+			sStdMenu += '</select>';
+			
+			var nLength = document.createElement( 'div' );
+			if ( !oSettings.aanFeatures.l )
+			{
+				nLength.id = oSettings.sTableId+'_length';
+			}
+			nLength.className = oSettings.oClasses.sLength;
+			nLength.innerHTML = '<label>'+oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu )+'</label>';
+			
+			/*
+			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
+			 * and Stefan Skopnik for fixing the fix!
+			 */
+			$('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true);
+			
+			$('select', nLength).bind( 'change.DT', function(e) {
+				var iVal = $(this).val();
+				
+				/* Update all other length options for the new display */
+				var n = oSettings.aanFeatures.l;
+				for ( i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != this.parentNode )
+					{
+						$('select', n[i]).val( iVal );
+					}
+				}
+				
+				/* Redraw the table */
+				oSettings._iDisplayLength = parseInt(iVal, 10);
+				_fnCalculateEnd( oSettings );
+				
+				/* If we have space to show extra rows (backing up from the end point - then do so */
+				if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;
+					if ( oSettings._iDisplayStart < 0 )
+					{
+						oSettings._iDisplayStart = 0;
+					}
+				}
+				
+				if ( oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+				
+				_fnDraw( oSettings );
+			} );
+		
+		
+			$('select', nLength).attr('aria-controls', oSettings.sTableId);
+			
+			return nLength;
+		}
+		
+		
+		/**
+		 * Recalculate the end point based on the start point
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateEnd( oSettings )
+		{
+			if ( oSettings.oFeatures.bPaginate === false )
+			{
+				oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+			}
+			else
+			{
+				/* Set the end point of the display - based on how many elements there are
+				 * still to display
+				 */
+				if ( oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||
+					   oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+				}
+				else
+				{
+					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
+				}
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Note that most of the paging logic is done in 
+		 * DataTable.ext.oPagination
+		 */
+		
+		/**
+		 * Generate the node required for default pagination
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Pagination feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlPaginate ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			var nPaginate = document.createElement( 'div' );
+			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
+			
+			DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, 
+				function( oSettings ) {
+					_fnCalculateEnd( oSettings );
+					_fnDraw( oSettings );
+				}
+			);
+			
+			/* Add a draw callback for the pagination on first instance, to update the paging display */
+			if ( !oSettings.aanFeatures.p )
+			{
+				oSettings.aoDrawCallback.push( {
+					"fn": function( oSettings ) {
+						DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
+							_fnCalculateEnd( oSettings );
+							_fnDraw( oSettings );
+						} );
+					},
+					"sName": "pagination"
+				} );
+			}
+			return nPaginate;
+		}
+		
+		
+		/**
+		 * Alter the display settings to change the page
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer)
+		 *  @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnPageChange ( oSettings, mAction )
+		{
+			var iOldStart = oSettings._iDisplayStart;
+			
+			if ( typeof mAction === "number" )
+			{
+				oSettings._iDisplayStart = mAction * oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "first" )
+			{
+				oSettings._iDisplayStart = 0;
+			}
+			else if ( mAction == "previous" )
+			{
+				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
+					oSettings._iDisplayStart - oSettings._iDisplayLength :
+					0;
+				
+				/* Correct for under-run */
+				if ( oSettings._iDisplayStart < 0 )
+				{
+				  oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "next" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					/* Make sure we are not over running the display array */
+					if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )
+					{
+						oSettings._iDisplayStart += oSettings._iDisplayLength;
+					}
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "last" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;
+					oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 0, "Unknown paging action: "+mAction );
+			}
+			$(oSettings.oInstance).trigger('page', oSettings);
+			
+			return iOldStart != oSettings._iDisplayStart;
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for the processing node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Processing element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlProcessing ( oSettings )
+		{
+			var nProcessing = document.createElement( 'div' );
+			
+			if ( !oSettings.aanFeatures.r )
+			{
+				nProcessing.id = oSettings.sTableId+'_processing';
+			}
+			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
+			nProcessing.className = oSettings.oClasses.sProcessing;
+			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
+			
+			return nProcessing;
+		}
+		
+		
+		/**
+		 * Display or hide the processing indicator
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bShow Show the processing indicator (true) or not (false)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnProcessingDisplay ( oSettings, bShow )
+		{
+			if ( oSettings.oFeatures.bProcessing )
+			{
+				var an = oSettings.aanFeatures.r;
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					an[i].style.visibility = bShow ? "visible" : "hidden";
+				}
+			}
+		
+			$(oSettings.oInstance).trigger('processing', [oSettings, bShow]);
+		}
+		
+		/**
+		 * Add any control elements for the table - specifically scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlTable ( oSettings )
+		{
+			/* Check if scrolling is enabled or not - if not then leave the DOM unaltered */
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				return oSettings.nTable;
+			}
+			
+			/*
+			 * The HTML structure that we want to generate in this function is:
+			 *  div - nScroller
+			 *    div - nScrollHead
+			 *      div - nScrollHeadInner
+			 *        table - nScrollHeadTable
+			 *          thead - nThead
+			 *    div - nScrollBody
+			 *      table - oSettings.nTable
+			 *        thead - nTheadSize
+			 *        tbody - nTbody
+			 *    div - nScrollFoot
+			 *      div - nScrollFootInner
+			 *        table - nScrollFootTable
+			 *          tfoot - nTfoot
+			 */
+			var
+			 	nScroller = document.createElement('div'),
+			 	nScrollHead = document.createElement('div'),
+			 	nScrollHeadInner = document.createElement('div'),
+			 	nScrollBody = document.createElement('div'),
+			 	nScrollFoot = document.createElement('div'),
+			 	nScrollFootInner = document.createElement('div'),
+			 	nScrollHeadTable = oSettings.nTable.cloneNode(false),
+			 	nScrollFootTable = oSettings.nTable.cloneNode(false),
+				nThead = oSettings.nTable.getElementsByTagName('thead')[0],
+			 	nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null : 
+					oSettings.nTable.getElementsByTagName('tfoot')[0],
+				oClasses = oSettings.oClasses;
+			
+			nScrollHead.appendChild( nScrollHeadInner );
+			nScrollFoot.appendChild( nScrollFootInner );
+			nScrollBody.appendChild( oSettings.nTable );
+			nScroller.appendChild( nScrollHead );
+			nScroller.appendChild( nScrollBody );
+			nScrollHeadInner.appendChild( nScrollHeadTable );
+			nScrollHeadTable.appendChild( nThead );
+			if ( nTfoot !== null )
+			{
+				nScroller.appendChild( nScrollFoot );
+				nScrollFootInner.appendChild( nScrollFootTable );
+				nScrollFootTable.appendChild( nTfoot );
+			}
+			
+			nScroller.className = oClasses.sScrollWrapper;
+			nScrollHead.className = oClasses.sScrollHead;
+			nScrollHeadInner.className = oClasses.sScrollHeadInner;
+			nScrollBody.className = oClasses.sScrollBody;
+			nScrollFoot.className = oClasses.sScrollFoot;
+			nScrollFootInner.className = oClasses.sScrollFootInner;
+			
+			if ( oSettings.oScroll.bAutoCss )
+			{
+				nScrollHead.style.overflow = "hidden";
+				nScrollHead.style.position = "relative";
+				nScrollFoot.style.overflow = "hidden";
+				nScrollBody.style.overflow = "auto";
+			}
+			
+			nScrollHead.style.border = "0";
+			nScrollHead.style.width = "100%";
+			nScrollFoot.style.border = "0";
+			nScrollHeadInner.style.width = oSettings.oScroll.sXInner !== "" ?
+				oSettings.oScroll.sXInner : "100%"; /* will be overwritten */
+			
+			/* Modify attributes to respect the clones */
+			nScrollHeadTable.removeAttribute('id');
+			nScrollHeadTable.style.marginLeft = "0";
+			oSettings.nTable.style.marginLeft = "0";
+			if ( nTfoot !== null )
+			{
+				nScrollFootTable.removeAttribute('id');
+				nScrollFootTable.style.marginLeft = "0";
+			}
+			
+			/* Move caption elements from the body to the header, footer or leave where it is
+			 * depending on the configuration. Note that the DTD says there can be only one caption */
+			var nCaption = $(oSettings.nTable).children('caption');
+			if ( nCaption.length > 0 )
+			{
+				nCaption = nCaption[0];
+				if ( nCaption._captionSide === "top" )
+				{
+					nScrollHeadTable.appendChild( nCaption );
+				}
+				else if ( nCaption._captionSide === "bottom" && nTfoot )
+				{
+					nScrollFootTable.appendChild( nCaption );
+				}
+			}
+			
+			/*
+			 * Sizing
+			 */
+			/* When x-scrolling add the width and a scroller to move the header with the body */
+			if ( oSettings.oScroll.sX !== "" )
+			{
+				nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				
+				if ( nTfoot !== null )
+				{
+					nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );	
+				}
+				
+				/* When the body is scrolled, then we also want to scroll the headers */
+				$(nScrollBody).scroll( function (e) {
+					nScrollHead.scrollLeft = this.scrollLeft;
+					
+					if ( nTfoot !== null )
+					{
+						nScrollFoot.scrollLeft = this.scrollLeft;
+					}
+				} );
+			}
+			
+			/* When yscrolling, add the height */
+			if ( oSettings.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );
+			}
+			
+			/* Redraw - align columns across the tables */
+			oSettings.aoDrawCallback.push( {
+				"fn": _fnScrollDraw,
+				"sName": "scrolling"
+			} );
+			
+			/* Infinite scrolling event handlers */
+			if ( oSettings.oScroll.bInfinite )
+			{
+				$(nScrollBody).scroll( function() {
+					/* Use a blocker to stop scrolling from loading more data while other data is still loading */
+					if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 )
+					{
+						/* Check if we should load the next data set */
+						if ( $(this).scrollTop() + $(this).height() > 
+							$(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )
+						{
+							/* Only do the redraw if we have to - we might be at the end of the data */
+							if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )
+							{
+								_fnPageChange( oSettings, 'next' );
+								_fnCalculateEnd( oSettings );
+								_fnDraw( oSettings );
+							}
+						}
+					}
+				} );
+			}
+			
+			oSettings.nScrollHead = nScrollHead;
+			oSettings.nScrollFoot = nScrollFoot;
+			
+			return nScroller;
+		}
+		
+		
+		/**
+		 * Update the various tables for resizing. It's a bit of a pig this function, but
+		 * basically the idea to:
+		 *   1. Re-create the table inside the scrolling div
+		 *   2. Take live measurements from the DOM
+		 *   3. Apply the measurements
+		 *   4. Clean up
+		 *  @param {object} o dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollDraw ( o )
+		{
+			var
+				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
+				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
+				nScrollBody = o.nTable.parentNode,
+				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
+				nTheadSize, nTfootSize,
+				iWidth, aApplied=[], aAppliedFooter=[], iSanityWidth,
+				nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null,
+				nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null,
+				ie67 = o.oBrowser.bScrollOversize,
+				zeroOut = function(nSizer) {
+					oStyle = nSizer.style;
+					oStyle.paddingTop = "0";
+					oStyle.paddingBottom = "0";
+					oStyle.borderTopWidth = "0";
+					oStyle.borderBottomWidth = "0";
+					oStyle.height = 0;
+				};
+			
+			/*
+			 * 1. Re-create the table inside the scrolling div
+			 */
+			
+			/* Remove the old minimised thead and tfoot elements in the inner table */
+			$(o.nTable).children('thead, tfoot').remove();
+		
+			/* Clone the current header and footer elements and then place it into the inner table */
+			nTheadSize = $(o.nTHead).clone()[0];
+			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
+			anHeadToSize = o.nTHead.getElementsByTagName('tr');
+			anHeadSizers = nTheadSize.getElementsByTagName('tr');
+			
+			if ( o.nTFoot !== null )
+			{
+				nTfootSize = $(o.nTFoot).clone()[0];
+				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
+				anFootToSize = o.nTFoot.getElementsByTagName('tr');
+				anFootSizers = nTfootSize.getElementsByTagName('tr');
+			}
+			
+			/*
+			 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+			 */
+			
+			/* Remove old sizing and apply the calculated column widths
+			 * Get the unique column headers in the newly created (cloned) header. We want to apply the
+			 * calculated sizes to this header
+			 */
+			if ( o.oScroll.sX === "" )
+			{
+				nScrollBody.style.width = '100%';
+				nScrollHeadInner.parentNode.style.width = '100%';
+			}
+			
+			var nThs = _fnGetUniqueThs( o, nTheadSize );
+			for ( i=0, iLen=nThs.length ; i<iLen ; i++ )
+			{
+				iVis = _fnVisibleToColumnIndex( o, i );
+				nThs[i].style.width = o.aoColumns[iVis].sWidth;
+			}
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(n) {
+					n.style.width = "";
+				}, anFootSizers );
+			}
+		
+			// If scroll collapse is enabled, when we put the headers back into the body for sizing, we
+			// will end up forcing the scrollbar to appear, making our measurements wrong for when we
+			// then hide it (end of this function), so add the header height to the body scroller.
+			if ( o.oScroll.bCollapse && o.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = (nScrollBody.offsetHeight + o.nTHead.offsetHeight)+"px";
+			}
+			
+			/* Size the table as a whole */
+			iSanityWidth = $(o.nTable).outerWidth();
+			if ( o.oScroll.sX === "" )
+			{
+				/* No x scrolling */
+				o.nTable.style.width = "100%";
+				
+				/* I know this is rubbish - but IE7 will make the width of the table when 100% include
+				 * the scrollbar - which is shouldn't. When there is a scrollbar we need to take this
+				 * into account.
+				 */
+				if ( ie67 && ($('tbody', nScrollBody).height() > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth);
+				}
+			}
+			else
+			{
+				if ( o.oScroll.sXInner !== "" )
+				{
+					/* x scroll inner has been given - use it */
+					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
+				}
+				else if ( iSanityWidth == $(nScrollBody).width() &&
+				   $(nScrollBody).height() < $(o.nTable).height() )
+				{
+					/* There is y-scrolling - try to take account of the y scroll bar */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );
+					if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )
+					{
+						/* Not possible to take account of it */
+						o.nTable.style.width = _fnStringToCss( iSanityWidth );
+					}
+				}
+				else
+				{
+					/* All else fails */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth );
+				}
+			}
+			
+			/* Recalculate the sanity width - now that we've applied the required width, before it was
+			 * a temporary variable. This is required because the column width calculation is done
+			 * before this table DOM is created.
+			 */
+			iSanityWidth = $(o.nTable).outerWidth();
+			
+			/* We want the hidden header to have zero height, so remove padding and borders. Then
+			 * set the width based on the real headers
+			 */
+			
+			// Apply all styles in one pass. Invalidates layout only once because we don't read any 
+			// DOM properties.
+			_fnApplyToChildren( zeroOut, anHeadSizers );
+			 
+			// Read all widths in next pass. Forces layout only once because we do not change 
+			// any DOM properties.
+			_fnApplyToChildren( function(nSizer) {
+				aApplied.push( _fnStringToCss( $(nSizer).width() ) );
+			}, anHeadSizers );
+			 
+			// Apply all widths in final pass. Invalidates layout only once because we do not
+			// read any DOM properties.
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = aApplied[i];
+			}, anHeadToSize );
+		
+			$(anHeadSizers).height(0);
+			
+			/* Same again with the footer if we have one */
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( zeroOut, anFootSizers );
+				 
+				_fnApplyToChildren( function(nSizer) {
+					aAppliedFooter.push( _fnStringToCss( $(nSizer).width() ) );
+				}, anFootSizers );
+				 
+				_fnApplyToChildren( function(nToSize, i) {
+					nToSize.style.width = aAppliedFooter[i];
+				}, anFootToSize );
+		
+				$(anFootSizers).height(0);
+			}
+			
+			/*
+			 * 3. Apply the measurements
+			 */
+			
+			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width
+			 * to what they currently are
+			 */
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = "";
+				nSizer.style.width = aApplied[i];
+			}, anHeadSizers );
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(nSizer, i) {
+					nSizer.innerHTML = "";
+					nSizer.style.width = aAppliedFooter[i];
+				}, anFootSizers );
+			}
+			
+			/* Sanity check that the table is of a sensible width. If not then we are going to get
+			 * misalignment - try to prevent this by not allowing the table to shrink below its min width
+			 */
+			if ( $(o.nTable).outerWidth() < iSanityWidth )
+			{
+				/* The min width depends upon if we have a vertical scrollbar visible or not */
+				var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")) ?
+						iSanityWidth+o.oScroll.iBarWidth : iSanityWidth;
+				
+				/* IE6/7 are a law unto themselves... */
+				if ( ie67 && (nScrollBody.scrollHeight > 
+					nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth );
+				}
+				
+				/* Apply the calculated minimum width to the table wrappers */
+				nScrollBody.style.width = _fnStringToCss( iCorrection );
+				o.nScrollHead.style.width = _fnStringToCss( iCorrection );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( iCorrection );
+				}
+				
+				/* And give the user a warning that we've stopped the table getting too small */
+				if ( o.oScroll.sX === "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. The table has been drawn at its minimum possible width." );
+				}
+				else if ( o.oScroll.sXInner !== "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. Increase the sScrollXInner value or remove it to allow automatic"+
+						" calculation" );
+				}
+			}
+			else
+			{
+				nScrollBody.style.width = _fnStringToCss( '100%' );
+				o.nScrollHead.style.width = _fnStringToCss( '100%' );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( '100%' );
+				}
+			}
+			
+			
+			/*
+			 * 4. Clean up
+			 */
+			if ( o.oScroll.sY === "" )
+			{
+				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+				 * the scrollbar height from the visible display, rather than adding it on. We need to
+				 * set the height in order to sort this. Don't want to do it in any other browsers.
+				 */
+				if ( ie67 )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );
+				}
+			}
+			
+			if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )
+			{
+				nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );
+				
+				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
+				 	o.oScroll.iBarWidth : 0;
+				if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra );
+				}
+			}
+			
+			/* Finally set the width's of the header and footer tables */
+			var iOuterWidth = $(o.nTable).outerWidth();
+			nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );
+			nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth );
+		
+			// Figure out if there are scrollbar present - if so then we need a the header and footer to
+			// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+			var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll";
+			nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			
+			if ( o.nTFoot !== null )
+			{
+				nScrollFootTable.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			}
+		
+			/* Adjust the position of the header in case we loose the y-scrollbar */
+			$(nScrollBody).scroll();
+			
+			/* If sorting or filtering has occurred, jump the scrolling back to the top */
+			if ( o.bSorted || o.bFiltered )
+			{
+				nScrollBody.scrollTop = 0;
+			}
+		}
+		
+		
+		/**
+		 * Apply a given function to the display child nodes of an element array (typically
+		 * TD children of TR rows
+		 *  @param {function} fn Method to apply to the objects
+		 *  @param array {nodes} an1 List of elements to look through for display children
+		 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyToChildren( fn, an1, an2 )
+		{
+			var index=0, i=0, iLen=an1.length;
+			var nNode1, nNode2;
+		
+			while ( i < iLen )
+			{
+				nNode1 = an1[i].firstChild;
+				nNode2 = an2 ? an2[i].firstChild : null;
+				while ( nNode1 )
+				{
+					if ( nNode1.nodeType === 1 )
+					{
+						if ( an2 )
+						{
+							fn( nNode1, nNode2, index );
+						}
+						else
+						{
+							fn( nNode1, index );
+						}
+						index++;
+					}
+					nNode1 = nNode1.nextSibling;
+					nNode2 = an2 ? nNode2.nextSibling : null;
+				}
+				i++;
+			}
+		}
+		
+		/**
+		 * Convert a CSS unit width to pixels (e.g. 2em)
+		 *  @param {string} sWidth width to be converted
+		 *  @param {node} nParent parent to get the with for (required for relative widths) - optional
+		 *  @returns {int} iWidth width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnConvertToWidth ( sWidth, nParent )
+		{
+			if ( !sWidth || sWidth === null || sWidth === '' )
+			{
+				return 0;
+			}
+			
+			if ( !nParent )
+			{
+				nParent = document.body;
+			}
+			
+			var iWidth;
+			var nTmp = document.createElement( "div" );
+			nTmp.style.width = _fnStringToCss( sWidth );
+			
+			nParent.appendChild( nTmp );
+			iWidth = nTmp.offsetWidth;
+			nParent.removeChild( nTmp );
+			
+			return ( iWidth );
+		}
+		
+		
+		/**
+		 * Calculate the width of columns for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateColumnWidths ( oSettings )
+		{
+			var iTableWidth = oSettings.nTable.offsetWidth;
+			var iUserInputs = 0;
+			var iTmpWidth;
+			var iVisibleColumns = 0;
+			var iColums = oSettings.aoColumns.length;
+			var i, iIndex, iCorrector, iWidth;
+			var oHeaders = $('th', oSettings.nTHead);
+			var widthAttr = oSettings.nTable.getAttribute('width');
+			var nWrapper = oSettings.nTable.parentNode;
+			
+			/* Convert any user input sizes into pixel sizes */
+			for ( i=0 ; i<iColums ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible )
+				{
+					iVisibleColumns++;
+					
+					if ( oSettings.aoColumns[i].sWidth !== null )
+					{
+						iTmpWidth = _fnConvertToWidth( oSettings.aoColumns[i].sWidthOrig, 
+							nWrapper );
+						if ( iTmpWidth !== null )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+						}
+							
+						iUserInputs++;
+					}
+				}
+			}
+			
+			/* If the number of columns in the DOM equals the number that we have to process in 
+			 * DataTables, then we can use the offsets that are created by the web-browser. No custom 
+			 * sizes can be set in order for this to happen, nor scrolling used
+			 */
+			if ( iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums &&
+				oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					iTmpWidth = $(oHeaders[i]).width();
+					if ( iTmpWidth !== null )
+					{
+						oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+					}
+				}
+			}
+			else
+			{
+				/* Otherwise we are going to have to do some calculations to get the width of each column.
+				 * Construct a 1 row table with the widest node in the data, and any user defined widths,
+				 * then insert it into the DOM and allow the browser to do all the hard work of
+				 * calculating table widths.
+				 */
+				var
+					nCalcTmp = oSettings.nTable.cloneNode( false ),
+					nTheadClone = oSettings.nTHead.cloneNode(true),
+					nBody = document.createElement( 'tbody' ),
+					nTr = document.createElement( 'tr' ),
+					nDivSizing;
+				
+				nCalcTmp.removeAttribute( "id" );
+				nCalcTmp.appendChild( nTheadClone );
+				if ( oSettings.nTFoot !== null )
+				{
+					nCalcTmp.appendChild( oSettings.nTFoot.cloneNode(true) );
+					_fnApplyToChildren( function(n) {
+						n.style.width = "";
+					}, nCalcTmp.getElementsByTagName('tr') );
+				}
+				
+				nCalcTmp.appendChild( nBody );
+				nBody.appendChild( nTr );
+				
+				/* Remove any sizing that was previously applied by the styles */
+				var jqColSizing = $('thead th', nCalcTmp);
+				if ( jqColSizing.length === 0 )
+				{
+					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);
+				}
+		
+				/* Apply custom sizing to the cloned header */
+				var nThs = _fnGetUniqueThs( oSettings, nTheadClone );
+				iCorrector = 0;
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					var oColumn = oSettings.aoColumns[i];
+					if ( oColumn.bVisible && oColumn.sWidthOrig !== null && oColumn.sWidthOrig !== "" )
+					{
+						nThs[i-iCorrector].style.width = _fnStringToCss( oColumn.sWidthOrig );
+					}
+					else if ( oColumn.bVisible )
+					{
+						nThs[i-iCorrector].style.width = "";
+					}
+					else
+					{
+						iCorrector++;
+					}
+				}
+		
+				/* Find the biggest td for each column and put it into the table */
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						var nTd = _fnGetWidestNode( oSettings, i );
+						if ( nTd !== null )
+						{
+							nTd = nTd.cloneNode(true);
+							if ( oSettings.aoColumns[i].sContentPadding !== "" )
+							{
+								nTd.innerHTML += oSettings.aoColumns[i].sContentPadding;
+							}
+							nTr.appendChild( nTd );
+						}
+					}
+				}
+				
+				/* Build the table and 'display' it */
+				nWrapper.appendChild( nCalcTmp );
+				
+				/* When scrolling (X or Y) we want to set the width of the table as appropriate. However,
+				 * when not scrolling leave the table width as it is. This results in slightly different,
+				 * but I think correct behaviour
+				 */
+				if ( oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);
+				}
+				else if ( oSettings.oScroll.sX !== "" )
+				{
+					nCalcTmp.style.width = "";
+					if ( $(nCalcTmp).width() < nWrapper.offsetWidth )
+					{
+						nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+					}
+				}
+				else if ( oSettings.oScroll.sY !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+				}
+				else if ( widthAttr )
+				{
+					nCalcTmp.style.width = _fnStringToCss( widthAttr );
+				}
+				nCalcTmp.style.visibility = "hidden";
+				
+				/* Scrolling considerations */
+				_fnScrollingWidthAdjust( oSettings, nCalcTmp );
+				
+				/* Read the width's calculated by the browser and store them for use by the caller. We
+				 * first of all try to use the elements in the body, but it is possible that there are
+				 * no elements there, under which circumstances we use the header elements
+				 */
+				var oNodes = $("tbody tr:eq(0)", nCalcTmp).children();
+				if ( oNodes.length === 0 )
+				{
+					oNodes = _fnGetUniqueThs( oSettings, $('thead', nCalcTmp)[0] );
+				}
+		
+				/* Browsers need a bit of a hand when a width is assigned to any columns when 
+				 * x-scrolling as they tend to collapse the table to the min-width, even if
+				 * we sent the column widths. So we need to keep track of what the table width
+				 * should be by summing the user given values, and the automatic values
+				 */
+				if ( oSettings.oScroll.sX !== "" )
+				{
+					var iTotal = 0;
+					iCorrector = 0;
+					for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+					{
+						if ( oSettings.aoColumns[i].bVisible )
+						{
+							if ( oSettings.aoColumns[i].sWidthOrig === null )
+							{
+								iTotal += $(oNodes[iCorrector]).outerWidth();
+							}
+							else
+							{
+								iTotal += parseInt(oSettings.aoColumns[i].sWidth.replace('px',''), 10) +
+									($(oNodes[iCorrector]).outerWidth() - $(oNodes[iCorrector]).width());
+							}
+							iCorrector++;
+						}
+					}
+					
+					nCalcTmp.style.width = _fnStringToCss( iTotal );
+					oSettings.nTable.style.width = _fnStringToCss( iTotal );
+				}
+		
+				iCorrector = 0;
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iWidth = $(oNodes[iCorrector]).width();
+						if ( iWidth !== null && iWidth > 0 )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
+						}
+						iCorrector++;
+					}
+				}
+		
+				var cssWidth = $(nCalcTmp).css('width');
+				oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ?
+				    cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() );
+				nCalcTmp.parentNode.removeChild( nCalcTmp );
+			}
+		
+			if ( widthAttr )
+			{
+				oSettings.nTable.style.width = _fnStringToCss( widthAttr );
+			}
+		}
+		
+		
+		/**
+		 * Adjust a table's width to take account of scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollingWidthAdjust ( oSettings, n )
+		{
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )
+			{
+				/* When y-scrolling only, we want to remove the width of the scroll bar so the table
+				 * + scroll bar will fit into the area avaialble.
+				 */
+				var iOrigWidth = $(n).width();
+				n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );
+			}
+			else if ( oSettings.oScroll.sX !== "" )
+			{
+				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */
+				n.style.width = _fnStringToCss( $(n).outerWidth() );
+			}
+		}
+		
+		
+		/**
+		 * Get the widest node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {node} widest table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetWidestNode( oSettings, iCol )
+		{
+			var iMaxIndex = _fnGetMaxLenString( oSettings, iCol );
+			if ( iMaxIndex < 0 )
+			{
+				return null;
+			}
+		
+			if ( oSettings.aoData[iMaxIndex].nTr === null )
+			{
+				var n = document.createElement('td');
+				n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' );
+				return n;
+			}
+			return _fnGetTdNodes(oSettings, iMaxIndex)[iCol];
+		}
+		
+		
+		/**
+		 * Get the maximum strlen for each data column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {string} max string length for each column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetMaxLenString( oSettings, iCol )
+		{
+			var iMax = -1;
+			var iMaxIndex = -1;
+			
+			for ( var i=0 ; i<oSettings.aoData.length ; i++ )
+			{
+				var s = _fnGetCellData( oSettings, i, iCol, 'display' )+"";
+				s = s.replace( /<.*?>/g, "" );
+				if ( s.length > iMax )
+				{
+					iMax = s.length;
+					iMaxIndex = i;
+				}
+			}
+			
+			return iMaxIndex;
+		}
+		
+		
+		/**
+		 * Append a CSS unit (only if required) to a string
+		 *  @param {array} aArray1 first array
+		 *  @param {array} aArray2 second array
+		 *  @returns {int} 0 if match, 1 if length is different, 2 if no match
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnStringToCss( s )
+		{
+			if ( s === null )
+			{
+				return "0px";
+			}
+			
+			if ( typeof s == 'number' )
+			{
+				if ( s < 0 )
+				{
+					return "0px";
+				}
+				return s+"px";
+			}
+			
+			/* Check if the last character is not 0-9 */
+			var c = s.charCodeAt( s.length-1 );
+			if (c < 0x30 || c > 0x39)
+			{
+				return s;
+			}
+			return s+"px";
+		}
+		
+		
+		/**
+		 * Get the width of a scroll bar in this browser being used
+		 *  @returns {int} width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollBarWidth ()
+		{  
+			var inner = document.createElement('p');
+			var style = inner.style;
+			style.width = "100%";
+			style.height = "200px";
+			style.padding = "0px";
+			
+			var outer = document.createElement('div');
+			style = outer.style;
+			style.position = "absolute";
+			style.top = "0px";
+			style.left = "0px";
+			style.visibility = "hidden";
+			style.width = "200px";
+			style.height = "150px";
+			style.padding = "0px";
+			style.overflow = "hidden";
+			outer.appendChild(inner);
+			
+			document.body.appendChild(outer);
+			var w1 = inner.offsetWidth;
+			outer.style.overflow = 'scroll';
+			var w2 = inner.offsetWidth;
+			if ( w1 == w2 )
+			{
+				w2 = outer.clientWidth;
+			}
+			
+			document.body.removeChild(outer);
+			return (w1 - w2);  
+		}
+		
+		/**
+		 * Change the order of the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bApplyClasses optional - should we apply classes or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSort ( oSettings, bApplyClasses )
+		{
+			var
+				i, iLen, j, jLen, k, kLen,
+				sDataType, nTh,
+				aaSort = [],
+			 	aiOrig = [],
+				oSort = DataTable.ext.oSort,
+				aoData = oSettings.aoData,
+				aoColumns = oSettings.aoColumns,
+				oAria = oSettings.oLanguage.oAria;
+			
+			/* No sorting required if server-side or no sorting array */
+			if ( !oSettings.oFeatures.bServerSide && 
+				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
+			{
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				/* If there is a sorting data type, and a function belonging to it, then we need to
+				 * get the data from the developer's function and apply it for this column
+				 */
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					var iColumn = aaSort[i][0];
+					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );
+					sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
+					if ( DataTable.ext.afnSortData[sDataType] )
+					{
+						var aData = DataTable.ext.afnSortData[sDataType].call( 
+							oSettings.oInstance, oSettings, iColumn, iVisColumn
+						);
+						if ( aData.length === aoData.length )
+						{
+							for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
+							{
+								_fnSetCellData( oSettings, j, iColumn, aData[j] );
+							}
+						}
+						else
+						{
+							_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" );
+						}
+					}
+				}
+				
+				/* Create a value - key array of the current row positions such that we can use their
+				 * current position during the sort, if values match, in order to perform stable sorting
+				 */
+				for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					aiOrig[ oSettings.aiDisplayMaster[i] ] = i;
+				}
+		
+				/* Build an internal data array which is specific to the sort, so we can get and prep
+				 * the data to be sorted only once, rather than needing to do it every time the sorting
+				 * function runs. This make the sorting function a very simple comparison
+				 */
+				var iSortLen = aaSort.length;
+				var fnSortFormat, aDataSort;
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					for ( j=0 ; j<iSortLen ; j++ )
+					{
+						aDataSort = aoColumns[ aaSort[j][0] ].aDataSort;
+		
+						for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+						{
+							sDataType = aoColumns[ aDataSort[k] ].sType;
+							fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ];
+							
+							aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ?
+								fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) :
+								_fnGetCellData( oSettings, i, aDataSort[k], 'sort' );
+						}
+					}
+				}
+				
+				/* Do the sort - here we want multi-column sorting based on a given data source (column)
+				 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+				 * follow on it's own, but this is what we want (example two column sorting):
+				 *  fnLocalSorting = function(a,b){
+				 *  	var iTest;
+				 *  	iTest = oSort['string-asc']('data11', 'data12');
+				 *  	if (iTest !== 0)
+				 *  		return iTest;
+				 *    iTest = oSort['numeric-desc']('data21', 'data22');
+				 *    if (iTest !== 0)
+				 *  		return iTest;
+				 *  	return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				 *  }
+				 * Basically we have a test for each sorting column, if the data in that column is equal,
+				 * test the next column. If all columns match, then we use a numeric sort on the row 
+				 * positions in the original data array to provide a stable sort.
+				 */
+				oSettings.aiDisplayMaster.sort( function ( a, b ) {
+					var k, l, lLen, iTest, aDataSort, sDataType;
+					for ( k=0 ; k<iSortLen ; k++ )
+					{
+						aDataSort = aoColumns[ aaSort[k][0] ].aDataSort;
+		
+						for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ )
+						{
+							sDataType = aoColumns[ aDataSort[l] ].sType;
+							
+							iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ](
+								aoData[a]._aSortData[ aDataSort[l] ],
+								aoData[b]._aSortData[ aDataSort[l] ]
+							);
+						
+							if ( iTest !== 0 )
+							{
+								return iTest;
+							}
+						}
+					}
+					
+					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				} );
+			}
+			
+			/* Alter the sorting classes to take account of the changes */
+			if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender )
+			{
+				_fnSortingClasses( oSettings );
+			}
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" );
+				nTh = aoColumns[i].nTh;
+				nTh.removeAttribute('aria-sort');
+				nTh.removeAttribute('aria-label');
+				
+				/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+				if ( aoColumns[i].bSortable )
+				{
+					if ( aaSort.length > 0 && aaSort[0][0] == i )
+					{
+						nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
+						
+						var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? 
+							aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
+						nTh.setAttribute('aria-label', sTitle+
+							(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+					else
+					{
+						nTh.setAttribute('aria-label', sTitle+
+							(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+				}
+				else
+				{
+					nTh.setAttribute('aria-label', sTitle);
+				}
+			}
+			
+			/* Tell the draw function that we have sorted the data */
+			oSettings.bSorted = true;
+			$(oSettings.oInstance).trigger('sort', oSettings);
+			
+			/* Copy the master data into the draw array and re-draw */
+			if ( oSettings.oFeatures.bFilter )
+			{
+				/* _fnFilter() will redraw the table for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Attach a sort handler (click) to a node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nNode node to attach the handler to
+		 *  @param {int} iDataIndex column sorting index
+		 *  @param {function} [fnCallback] callback function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
+		{
+			_fnBindAction( nNode, {}, function (e) {
+				/* If the column is not sortable - don't to anything */
+				if ( oSettings.aoColumns[iDataIndex].bSortable === false )
+				{
+					return;
+				}
+				
+				/*
+				 * This is a little bit odd I admit... I declare a temporary function inside the scope of
+				 * _fnBuildHead and the click handler in order that the code presented here can be used 
+				 * twice - once for when bProcessing is enabled, and another time for when it is 
+				 * disabled, as we need to perform slightly different actions.
+				 *   Basically the issue here is that the Javascript engine in modern browsers don't 
+				 * appear to allow the rendering engine to update the display while it is still executing
+				 * it's thread (well - it does but only after long intervals). This means that the 
+				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
+				 * I force an execution break by using setTimeout - but this breaks the expected 
+				 * thread continuation for the end-developer's point of view (their code would execute
+				 * too early), so we only do it when we absolutely have to.
+				 */
+				var fnInnerSorting = function () {
+					var iColumn, iNextSort;
+					
+					/* If the shift key is pressed then we are multiple column sorting */
+					if ( e.shiftKey )
+					{
+						/* Are we already doing some kind of sort on this column? */
+						var bFound = false;
+						for ( var i=0 ; i<oSettings.aaSorting.length ; i++ )
+						{
+							if ( oSettings.aaSorting[i][0] == iDataIndex )
+							{
+								bFound = true;
+								iColumn = oSettings.aaSorting[i][0];
+								iNextSort = oSettings.aaSorting[i][2]+1;
+								
+								if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+								{
+									/* Reached the end of the sorting options, remove from multi-col sort */
+									oSettings.aaSorting.splice( i, 1 );
+								}
+								else
+								{
+									/* Move onto next sorting direction */
+									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+									oSettings.aaSorting[i][2] = iNextSort;
+								}
+								break;
+							}
+						}
+						
+						/* No sort yet - add it in */
+						if ( bFound === false )
+						{
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					else
+					{
+						/* If no shift key then single column sort */
+						if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex )
+						{
+							iColumn = oSettings.aaSorting[0][0];
+							iNextSort = oSettings.aaSorting[0][2]+1;
+							if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+							{
+								iNextSort = 0;
+							}
+							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+							oSettings.aaSorting[0][2] = iNextSort;
+						}
+						else
+						{
+							oSettings.aaSorting.splice( 0, oSettings.aaSorting.length );
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					
+					/* Run the sort */
+					_fnSort( oSettings );
+				}; /* /fnInnerSorting */
+				
+				if ( !oSettings.oFeatures.bProcessing )
+				{
+					fnInnerSorting();
+				}
+				else
+				{
+					_fnProcessingDisplay( oSettings, true );
+					setTimeout( function() {
+						fnInnerSorting();
+						if ( !oSettings.oFeatures.bServerSide )
+						{
+							_fnProcessingDisplay( oSettings, false );
+						}
+					}, 0 );
+				}
+				
+				/* Call the user specified callback function - used for async user interaction */
+				if ( typeof fnCallback == 'function' )
+				{
+					fnCallback( oSettings );
+				}
+			} );
+		}
+		
+		
+		/**
+		 * Set the sorting classes on the header, Note: it is safe to call this function 
+		 * when bSort and bSortClasses are false
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortingClasses( oSettings )
+		{
+			var i, iLen, j, jLen, iFound;
+			var aaSort, sClass;
+			var iColumns = oSettings.aoColumns.length;
+			var oClasses = oSettings.oClasses;
+			
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc +
+						" "+ oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			if ( oSettings.aaSortingFixed !== null )
+			{
+				aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );
+			}
+			else
+			{
+				aaSort = oSettings.aaSorting.slice();
+			}
+			
+			/* Apply the required classes to the header */
+			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					sClass = oSettings.aoColumns[i].sSortingClass;
+					iFound = -1;
+					for ( j=0 ; j<aaSort.length ; j++ )
+					{
+						if ( aaSort[j][0] == i )
+						{
+							sClass = ( aaSort[j][1] == "asc" ) ?
+								oClasses.sSortAsc : oClasses.sSortDesc;
+							iFound = j;
+							break;
+						}
+					}
+					$(oSettings.aoColumns[i].nTh).addClass( sClass );
+					
+					if ( oSettings.bJUI )
+					{
+						/* jQuery UI uses extra markup */
+						var jqSpan = $("span."+oClasses.sSortIcon,  oSettings.aoColumns[i].nTh);
+						jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ 
+							oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed );
+						
+						var sSpanClass;
+						if ( iFound == -1 )
+						{
+						 	sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;
+						}
+						else if ( aaSort[iFound][1] == "asc" )
+						{
+							sSpanClass = oClasses.sSortJUIAsc;
+						}
+						else
+						{
+							sSpanClass = oClasses.sSortJUIDesc;
+						}
+						
+						jqSpan.addClass( sSpanClass );
+					}
+				}
+				else
+				{
+					/* No sorting on this column, so add the base class. This will have been assigned by
+					 * _fnAddColumn
+					 */
+					$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			/* 
+			 * Apply the required classes to the table body
+			 * Note that this is given as a feature switch since it can significantly slow down a sort
+			 * on large data sets (adding and removing of classes is always slow at the best of times..)
+			 * Further to this, note that this code is admittedly fairly ugly. It could be made a lot 
+			 * simpler using jQuery selectors and add/removeClass, but that is significantly slower
+			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
+			 * Note that for deferred drawing we do use jQuery - the reason being that taking the first
+			 * row found to see if the whole column needs processed can miss classes since the first
+			 * column might be new.
+			 */
+			sClass = oClasses.sSortColumn;
+			
+			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )
+			{
+				var nTds = _fnGetTdNodes( oSettings );
+				
+				/* Determine what the sorting class for each column should be */
+				var iClass, iTargetCol;
+				var asClasses = [];
+				for (i = 0; i < iColumns; i++)
+				{
+					asClasses.push("");
+				}
+				for (i = 0, iClass = 1; i < aaSort.length; i++)
+				{
+					iTargetCol = parseInt( aaSort[i][0], 10 );
+					asClasses[iTargetCol] = sClass + iClass;
+					
+					if ( iClass < 3 )
+					{
+						iClass++;
+					}
+				}
+				
+				/* Make changes to the classes for each cell as needed */
+				var reClass = new RegExp(sClass + "[123]");
+				var sTmpClass, sCurrentClass, sNewClass;
+				for ( i=0, iLen=nTds.length; i<iLen; i++ )
+				{
+					/* Determine which column we're looking at */
+					iTargetCol = i % iColumns;
+					
+					/* What is the full list of classes now */
+					sCurrentClass = nTds[i].className;
+					/* What sorting class should be applied? */
+					sNewClass = asClasses[iTargetCol];
+					/* What would the new full list be if we did a replacement? */
+					sTmpClass = sCurrentClass.replace(reClass, sNewClass);
+					
+					if ( sTmpClass != sCurrentClass )
+					{
+						/* We changed something */
+						nTds[i].className = $.trim( sTmpClass );
+					}
+					else if ( sNewClass.length > 0 && sCurrentClass.indexOf(sNewClass) == -1 )
+					{
+						/* We need to add a class */
+						nTds[i].className = sCurrentClass + " " + sNewClass;
+					}
+				}
+			}
+		}
+		
+		
+		
+		/**
+		 * Save the state of a table in a cookie such that the page can be reloaded
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSaveState ( oSettings )
+		{
+			if ( !oSettings.oFeatures.bStateSave || oSettings.bDestroying )
+			{
+				return;
+			}
+		
+			/* Store the interesting variables */
+			var i, iLen, bInfinite=oSettings.oScroll.bInfinite;
+			var oState = {
+				"iCreate":      new Date().getTime(),
+				"iStart":       (bInfinite ? 0 : oSettings._iDisplayStart),
+				"iEnd":         (bInfinite ? oSettings._iDisplayLength : oSettings._iDisplayEnd),
+				"iLength":      oSettings._iDisplayLength,
+				"aaSorting":    $.extend( true, [], oSettings.aaSorting ),
+				"oSearch":      $.extend( true, {}, oSettings.oPreviousSearch ),
+				"aoSearchCols": $.extend( true, [], oSettings.aoPreSearchCols ),
+				"abVisCols":    []
+			};
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oState.abVisCols.push( oSettings.aoColumns[i].bVisible );
+			}
+		
+			_fnCallbackFire( oSettings, "aoStateSaveParams", 'stateSaveParams', [oSettings, oState] );
+			
+			oSettings.fnStateSave.call( oSettings.oInstance, oSettings, oState );
+		}
+		
+		
+		/**
+		 * Attempt to load a saved table state from a cookie
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oInit DataTables init object so we can override settings
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLoadState ( oSettings, oInit )
+		{
+			if ( !oSettings.oFeatures.bStateSave )
+			{
+				return;
+			}
+		
+			var oData = oSettings.fnStateLoad.call( oSettings.oInstance, oSettings );
+			if ( !oData )
+			{
+				return;
+			}
+			
+			/* Allow custom and plug-in manipulation functions to alter the saved data set and
+			 * cancelling of loading by returning false
+			 */
+			var abStateLoad = _fnCallbackFire( oSettings, 'aoStateLoadParams', 'stateLoadParams', [oSettings, oData] );
+			if ( $.inArray( false, abStateLoad ) !== -1 )
+			{
+				return;
+			}
+			
+			/* Store the saved state so it might be accessed at any time */
+			oSettings.oLoadedState = $.extend( true, {}, oData );
+			
+			/* Restore key features */
+			oSettings._iDisplayStart    = oData.iStart;
+			oSettings.iInitDisplayStart = oData.iStart;
+			oSettings._iDisplayEnd      = oData.iEnd;
+			oSettings._iDisplayLength   = oData.iLength;
+			oSettings.aaSorting         = oData.aaSorting.slice();
+			oSettings.saved_aaSorting   = oData.aaSorting.slice();
+			
+			/* Search filtering  */
+			$.extend( oSettings.oPreviousSearch, oData.oSearch );
+			$.extend( true, oSettings.aoPreSearchCols, oData.aoSearchCols );
+			
+			/* Column visibility state
+			 * Pass back visibility settings to the init handler, but to do not here override
+			 * the init object that the user might have passed in
+			 */
+			oInit.saved_aoColumns = [];
+			for ( var i=0 ; i<oData.abVisCols.length ; i++ )
+			{
+				oInit.saved_aoColumns[i] = {};
+				oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
+			}
+		
+			_fnCallbackFire( oSettings, 'aoStateLoaded', 'stateLoaded', [oSettings, oData] );
+		}
+		
+		
+		/**
+		 * Create a new cookie with a value to store the state of a table
+		 *  @param {string} sName name of the cookie to create
+		 *  @param {string} sValue the value the cookie should take
+		 *  @param {int} iSecs duration of the cookie
+		 *  @param {string} sBaseName sName is made up of the base + file name - this is the base
+		 *  @param {function} fnCallback User definable function to modify the cookie
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )
+		{
+			var date = new Date();
+			date.setTime( date.getTime()+(iSecs*1000) );
+			
+			/* 
+			 * Shocking but true - it would appear IE has major issues with having the path not having
+			 * a trailing slash on it. We need the cookie to be available based on the path, so we
+			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the
+			 * patch to use at least some of the path
+			 */
+			var aParts = window.location.pathname.split('/');
+			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g,"").toLowerCase();
+			var sFullCookie, oData;
+			
+			if ( fnCallback !== null )
+			{
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON( sValue ) : eval( '('+sValue+')' );
+				sFullCookie = fnCallback( sNameFile, oData, date.toGMTString(),
+					aParts.join('/')+"/" );
+			}
+			else
+			{
+				sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +
+					"; expires=" + date.toGMTString() +"; path=" + aParts.join('/')+"/";
+			}
+			
+			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies
+			 * belonging to DataTables.
+			 */
+			var
+				aCookies =document.cookie.split(';'),
+				iNewCookieLen = sFullCookie.split(';')[0].length,
+				aOldCookies = [];
+			
+			if ( iNewCookieLen+document.cookie.length+10 > 4096 ) /* Magic 10 for padding */
+			{
+				for ( var i=0, iLen=aCookies.length ; i<iLen ; i++ )
+				{
+					if ( aCookies[i].indexOf( sBaseName ) != -1 )
+					{
+						/* It's a DataTables cookie, so eval it and check the time stamp */
+						var aSplitCookie = aCookies[i].split('=');
+						try {
+							oData = eval( '('+decodeURIComponent(aSplitCookie[1])+')' );
+		
+							if ( oData && oData.iCreate )
+							{
+								aOldCookies.push( {
+									"name": aSplitCookie[0],
+									"time": oData.iCreate
+								} );
+							}
+						}
+						catch( e ) {}
+					}
+				}
+		
+				// Make sure we delete the oldest ones first
+				aOldCookies.sort( function (a, b) {
+					return b.time - a.time;
+				} );
+		
+				// Eliminate as many old DataTables cookies as we need to
+				while ( iNewCookieLen + document.cookie.length + 10 > 4096 ) {
+					if ( aOldCookies.length === 0 ) {
+						// Deleted all DT cookies and still not enough space. Can't state save
+						return;
+					}
+					
+					var old = aOldCookies.pop();
+					document.cookie = old.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+
+						aParts.join('/') + "/";
+				}
+			}
+			
+			document.cookie = sFullCookie;
+		}
+		
+		
+		/**
+		 * Read an old cookie to get a cookie with an old table state
+		 *  @param {string} sName name of the cookie to read
+		 *  @returns {string} contents of the cookie - or null if no cookie with that name found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReadCookie ( sName )
+		{
+			var
+				aParts = window.location.pathname.split('/'),
+				sNameEQ = sName + '_' + aParts[aParts.length-1].replace(/[\/:]/g,"").toLowerCase() + '=',
+			 	sCookieContents = document.cookie.split(';');
+			
+			for( var i=0 ; i<sCookieContents.length ; i++ )
+			{
+				var c = sCookieContents[i];
+				
+				while (c.charAt(0)==' ')
+				{
+					c = c.substring(1,c.length);
+				}
+				
+				if (c.indexOf(sNameEQ) === 0)
+				{
+					return decodeURIComponent( c.substring(sNameEQ.length,c.length) );
+				}
+			}
+			return null;
+		}
+		
+		
+		/**
+		 * Return the settings object for a particular table
+		 *  @param {node} nTable table we are using as a dataTable
+		 *  @returns {object} Settings object - or null if not found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSettingsFromNode ( nTable )
+		{
+			for ( var i=0 ; i<DataTable.settings.length ; i++ )
+			{
+				if ( DataTable.settings[i].nTable === nTable )
+				{
+					return DataTable.settings[i];
+				}
+			}
+			
+			return null;
+		}
+		
+		
+		/**
+		 * Return an array with the TR nodes for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {array} TR array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTrNodes ( oSettings )
+		{
+			var aNodes = [];
+			var aoData = oSettings.aoData;
+			for ( var i=0, iLen=aoData.length ; i<iLen ; i++ )
+			{
+				if ( aoData[i].nTr !== null )
+				{
+					aNodes.push( aoData[i].nTr );
+				}
+			}
+			return aNodes;
+		}
+		
+		
+		/**
+		 * Return an flat array with all TD nodes for the table, or row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} [iIndividualRow] aoData index to get the nodes for - optional 
+		 *    if not given then the return array will contain all nodes for the table
+		 *  @returns {array} TD array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTdNodes ( oSettings, iIndividualRow )
+		{
+			var anReturn = [];
+			var iCorrector;
+			var anTds, nTd;
+			var iRow, iRows=oSettings.aoData.length,
+				iColumn, iColumns, oData, sNodeName, iStart=0, iEnd=iRows;
+			
+			/* Allow the collection to be limited to just one row */
+			if ( iIndividualRow !== undefined )
+			{
+				iStart = iIndividualRow;
+				iEnd = iIndividualRow+1;
+			}
+		
+			for ( iRow=iStart ; iRow<iEnd ; iRow++ )
+			{
+				oData = oSettings.aoData[iRow];
+				if ( oData.nTr !== null )
+				{
+					/* get the TD child nodes - taking into account text etc nodes */
+					anTds = [];
+					nTd = oData.nTr.firstChild;
+					while ( nTd )
+					{
+						sNodeName = nTd.nodeName.toLowerCase();
+						if ( sNodeName == 'td' || sNodeName == 'th' )
+						{
+							anTds.push( nTd );
+						}
+						nTd = nTd.nextSibling;
+					}
+		
+					iCorrector = 0;
+					for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+					{
+						if ( oSettings.aoColumns[iColumn].bVisible )
+						{
+							anReturn.push( anTds[iColumn-iCorrector] );
+						}
+						else
+						{
+							anReturn.push( oData._anHidden[iColumn] );
+							iCorrector++;
+						}
+					}
+				}
+			}
+		
+			return anReturn;
+		}
+		
+		
+		/**
+		 * Log an error message
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iLevel log error messages, or display them to the user
+		 *  @param {string} sMesg error message
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLog( oSettings, iLevel, sMesg )
+		{
+			var sAlert = (oSettings===null) ?
+				"DataTables warning: "+sMesg :
+				"DataTables warning (table id = '"+oSettings.sTableId+"'): "+sMesg;
+			
+			if ( iLevel === 0 )
+			{
+				if ( DataTable.ext.sErrMode == 'alert' )
+				{
+					alert( sAlert );
+				}
+				else
+				{
+					throw new Error(sAlert);
+				}
+				return;
+			}
+			else if ( window.console && console.log )
+			{
+				console.log( sAlert );
+			}
+		}
+		
+		
+		/**
+		 * See if a property is defined on one object, if so assign it to the other object
+		 *  @param {object} oRet target object
+		 *  @param {object} oSrc source object
+		 *  @param {string} sName property
+		 *  @param {string} [sMappedName] name to map too - optional, sName used if not given
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnMap( oRet, oSrc, sName, sMappedName )
+		{
+			if ( sMappedName === undefined )
+			{
+				sMappedName = sName;
+			}
+			if ( oSrc[sName] !== undefined )
+			{
+				oRet[sMappedName] = oSrc[sName];
+			}
+		}
+		
+		
+		/**
+		 * Extend objects - very similar to jQuery.extend, but deep copy objects, and shallow
+		 * copy arrays. The reason we need to do this, is that we don't want to deep copy array
+		 * init values (such as aaSorting) since the dev wouldn't be able to override them, but
+		 * we do want to deep copy arrays.
+		 *  @param {object} oOut Object to extend
+		 *  @param {object} oExtender Object from which the properties will be applied to oOut
+		 *  @returns {object} oOut Reference, just for convenience - oOut === the return.
+		 *  @memberof DataTable#oApi
+		 *  @todo This doesn't take account of arrays inside the deep copied objects.
+		 */
+		function _fnExtend( oOut, oExtender )
+		{
+			var val;
+			
+			for ( var prop in oExtender )
+			{
+				if ( oExtender.hasOwnProperty(prop) )
+				{
+					val = oExtender[prop];
+		
+					if ( typeof oInit[prop] === 'object' && val !== null && $.isArray(val) === false )
+					{
+						$.extend( true, oOut[prop], val );
+					}
+					else
+					{
+						oOut[prop] = val;
+					}
+				}
+			}
+		
+			return oOut;
+		}
+		
+		
+		/**
+		 * Bind an event handers to allow a click or return key to activate the callback.
+		 * This is good for accessibility since a return on the keyboard will have the
+		 * same effect as a click, if the element has focus.
+		 *  @param {element} n Element to bind the action to
+		 *  @param {object} oData Data object to pass to the triggered function
+		 *  @param {function} fn Callback function for when the event is triggered
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBindAction( n, oData, fn )
+		{
+			$(n)
+				.bind( 'click.DT', oData, function (e) {
+						n.blur(); // Remove focus outline for mouse users
+						fn(e);
+					} )
+				.bind( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						fn(e);
+					} } )
+				.bind( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+					} );
+		}
+		
+		
+		/**
+		 * Register a callback function. Easily allows a callback function to be added to
+		 * an array store of callback functions that can then all be called together.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {function} fn Function to be called back
+		 *  @param {string} sName Identifying name for the callback (i.e. a label)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackReg( oSettings, sStore, fn, sName )
+		{
+			if ( fn )
+			{
+				oSettings[sStore].push( {
+					"fn": fn,
+					"sName": sName
+				} );
+			}
+		}
+		
+		
+		/**
+		 * Fire callback functions and trigger events. Note that the loop over the callback
+		 * array store is done backwards! Further note that you do not want to fire off triggers
+		 * in time sensitive applications (for example cell creation) as its slow.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {string} sTrigger Name of the jQuery custom event to trigger. If null no trigger
+		 *    is fired
+		 *  @param {array} aArgs Array of arguments to pass to the callback function / trigger
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackFire( oSettings, sStore, sTrigger, aArgs )
+		{
+			var aoStore = oSettings[sStore];
+			var aRet =[];
+		
+			for ( var i=aoStore.length-1 ; i>=0 ; i-- )
+			{
+				aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) );
+			}
+		
+			if ( sTrigger !== null )
+			{
+				$(oSettings.oInstance).trigger(sTrigger, aArgs);
+			}
+		
+			return aRet;
+		}
+		
+		
+		/**
+		 * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other
+		 * library, then we use that as it is fast, safe and accurate. If the function isn't 
+		 * available then we need to built it ourselves - the inspiration for this function comes
+		 * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is
+		 * not perfect and absolutely should not be used as a replacement to json2.js - but it does
+		 * do what we need, without requiring a dependency for DataTables.
+		 *  @param {object} o JSON object to be converted
+		 *  @returns {string} JSON string
+		 *  @memberof DataTable#oApi
+		 */
+		var _fnJsonString = (window.JSON) ? JSON.stringify : function( o )
+		{
+			/* Not an object or array */
+			var sType = typeof o;
+			if (sType !== "object" || o === null)
+			{
+				// simple data type
+				if (sType === "string")
+				{
+					o = '"'+o+'"';
+				}
+				return o+"";
+			}
+		
+			/* If object or array, need to recurse over it */
+			var
+				sProp, mValue,
+				json = [],
+				bArr = $.isArray(o);
+			
+			for (sProp in o)
+			{
+				mValue = o[sProp];
+				sType = typeof mValue;
+		
+				if (sType === "string")
+				{
+					mValue = '"'+mValue+'"';
+				}
+				else if (sType === "object" && mValue !== null)
+				{
+					mValue = _fnJsonString(mValue);
+				}
+		
+				json.push((bArr ? "" : '"'+sProp+'":') + mValue);
+			}
+		
+			return (bArr ? "[" : "{") + json + (bArr ? "]" : "}");
+		};
+		
+		
+		/**
+		 * From some browsers (specifically IE6/7) we need special handling to work around browser
+		 * bugs - this function is used to detect when these workarounds are needed.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBrowserDetect( oSettings )
+		{
+			/* IE6/7 will oversize a width 100% element inside a scrolling element, to include the
+			 * width of the scrollbar, while other browsers ensure the inner element is contained
+			 * without forcing scrolling
+			 */
+			var n = $(
+				'<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden">'+
+					'<div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;">'+
+						'<div id="DT_BrowserTest" style="width:100%; height:10px;"></div>'+
+					'</div>'+
+				'</div>')[0];
+		
+			document.body.appendChild( n );
+			oSettings.oBrowser.bScrollOversize = $('#DT_BrowserTest', n)[0].offsetWidth === 100 ? true : false;
+			document.body.removeChild( n );
+		}
+		
+
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			var i, iLen, a = [], tr;
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoData = oSettings.aoData;
+			var aiDisplay = oSettings.aiDisplay;
+			var aiDisplayMaster = oSettings.aiDisplayMaster;
+		
+			if ( !oOpts )
+			{
+				oOpts = {};
+			}
+		
+			oOpts = $.extend( {}, {
+				"filter": "none", // applied
+				"order": "current", // "original"
+				"page": "all" // current
+			}, oOpts );
+		
+			// Current page implies that order=current and fitler=applied, since it is fairly
+			// senseless otherwise
+			if ( oOpts.page == 'current' )
+			{
+				for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplayMaster[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aiDisplay.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr ;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr;
+					if ( $.inArray( i, aiDisplay ) !== -1 && tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 1, "Unknown selection options" );
+			}
+		
+			/* We need to filter on the TR elements and also 'find' in their descendants
+			 * to make the selector act like it would in a full table - so we need
+			 * to build both results and then combine them together
+			 */
+			var jqA = $(a);
+			var jqTRs = jqA.filter( sSelector );
+			var jqDescendants = jqA.find( sSelector );
+		
+			return $( [].concat($.makeArray(jqTRs), $.makeArray(jqDescendants)) );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to  
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null 
+		 *    entry in the array.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for 
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"filter": "applied"});
+		 *      
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the filter" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			var aOut = [];
+			var i, iLen, iIndex;
+			var aTrs = this.$( sSelector, oOpts );
+		
+			for ( i=0, iLen=aTrs.length ; i<iLen ; i++ )
+			{
+				aOut.push( this.fnGetData(aTrs[i]) );
+			}
+		
+			return aOut;
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using 
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} mData The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in 
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to 
+		 *    the table.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *    
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *    
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *        
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( mData, bRedraw )
+		{
+			if ( mData.length === 0 )
+			{
+				return [];
+			}
+			
+			var aiReturn = [];
+			var iTest;
+			
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			/* Check if we want to add multiple rows or not */
+			if ( typeof mData[0] === "object" && mData[0] !== null )
+			{
+				for ( var i=0 ; i<mData.length ; i++ )
+				{
+					iTest = _fnAddData( oSettings, mData[i] );
+					if ( iTest == -1 )
+					{
+						return aiReturn;
+					}
+					aiReturn.push( iTest );
+				}
+			}
+			else
+			{
+				iTest = _fnAddData( oSettings, mData );
+				if ( iTest == -1 )
+				{
+					return aiReturn;
+				}
+				aiReturn.push( iTest );
+			}
+			
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return aiReturn;
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data 
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or 
+		 * through the sWidth parameter). This can be useful when the width of the table's 
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      $(window).bind('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode(this[DataTable.ext.iApiIndex]);
+			_fnAdjustColumnSizing( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				this.fnDraw( false );
+			}
+			else if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				this.oApi._fnScrollDraw(oSettings);
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnClearTable( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which 
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
+					if ( nTrParent )
+					{
+						/* Remove it if it is currently on display */
+						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );
+					}
+					oSettings.aoOpenRows.splice( i, 1 );
+					return 0;
+				}
+			}
+			return 1;
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} mTarget The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [fnCallBack] Callback function
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, iAODataIndex;
+			
+			iAODataIndex = (typeof mTarget === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
+			
+			/* Return the data array from this row */
+			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
+		
+			/* Update the _DT_RowIndex parameter */
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					oSettings.aoData[i].nTr._DT_RowIndex = i;
+				}
+			}
+			
+			/* Remove the target row from the search array */
+			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );
+			oSettings.asDataSearch.splice( iDisplayIndex, 1 );
+			
+			/* Delete from the display arrays */
+			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );
+			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );
+			
+			/* If there is a user callback function - call it */
+			if ( typeof fnCallBack === "function" )
+			{
+				fnCallBack.call( this, oSettings, oData );
+			}
+			
+			/* Check for an 'overflow' they case for displaying the table */
+			if ( oSettings._iDisplayStart >= oSettings.fnRecordsDisplay() )
+			{
+				oSettings._iDisplayStart -= oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart < 0 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			return oData;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables 
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [bRemove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( bRemove )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var nOrig = oSettings.nTableWrapper.parentNode;
+			var nBody = oSettings.nTBody;
+			var i, iLen;
+		
+			bRemove = (bRemove===undefined) ? false : bRemove;
+			
+			/* Flag to note that the table is currently being destroyed - no action should be taken */
+			oSettings.bDestroying = true;
+			
+			/* Fire off the destroy callbacks for plug-ins etc */
+			_fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] );
+		
+			/* If the table is not being removed, restore the hidden columns */
+			if ( !bRemove )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible === false )
+					{
+						this.fnSetColumnVis( i, true );
+					}
+				}
+			}
+			
+			/* Blitz all DT events */
+			$(oSettings.nTableWrapper).find('*').andSelf().unbind('.DT');
+			
+			/* If there is an 'empty' indicator row, remove it */
+			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
+			
+			/* When scrolling we had to break the table up - restore it */
+			if ( oSettings.nTable != oSettings.nTHead.parentNode )
+			{
+				$(oSettings.nTable).children('thead').remove();
+				oSettings.nTable.appendChild( oSettings.nTHead );
+			}
+			
+			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
+			{
+				$(oSettings.nTable).children('tfoot').remove();
+				oSettings.nTable.appendChild( oSettings.nTFoot );
+			}
+			
+			/* Remove the DataTables generated nodes, events and classes */
+			oSettings.nTable.parentNode.removeChild( oSettings.nTable );
+			$(oSettings.nTableWrapper).remove();
+			
+			oSettings.aaSorting = [];
+			oSettings.aaSortingFixed = [];
+			_fnSortingClasses( oSettings );
+			
+			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') );
+			
+			$('th, td', oSettings.nTHead).removeClass( [
+				oSettings.oClasses.sSortable,
+				oSettings.oClasses.sSortableAsc,
+				oSettings.oClasses.sSortableDesc,
+				oSettings.oClasses.sSortableNone ].join(' ')
+			);
+			if ( oSettings.bJUI )
+			{
+				$('th span.'+oSettings.oClasses.sSortIcon
+					+ ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove();
+		
+				$('th, td', oSettings.nTHead).each( function () {
+					var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this);
+					var kids = jqWrapper.contents();
+					$(this).append( kids );
+					jqWrapper.remove();
+				} );
+			}
+			
+			/* Add the TR elements back into the table in their original order */
+			if ( !bRemove && oSettings.nTableReinsertBefore )
+			{
+				nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore );
+			}
+			else if ( !bRemove )
+			{
+				nOrig.appendChild( oSettings.nTable );
+			}
+		
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					nBody.appendChild( oSettings.aoData[i].nTr );
+				}
+			}
+			
+			/* Restore the width of the original table */
+			if ( oSettings.oFeatures.bAutoWidth === true )
+			{
+			  oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
+			}
+			
+			/* If the were originally stripe classes - then we add them back here. Note
+			 * this is not fool proof (for example if not all rows had stripe classes - but
+			 * it's a good effort without getting carried away
+			 */
+			iLen = oSettings.asDestroyStripes.length;
+			if (iLen)
+			{
+				var anRows = $(nBody).children('tr');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					anRows.filter(':nth-child(' + iLen + 'n + ' + i + ')').addClass( oSettings.asDestroyStripes[i] );
+				}
+			}
+			
+			/* Remove the settings object from the settings array */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				if ( DataTable.settings[i] == oSettings )
+				{
+					DataTable.settings.splice( i, 1 );
+				}
+			}
+			
+			/* End it all */
+			oSettings = null;
+			oInit = null;
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [bComplete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( bComplete )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			if ( bComplete === false )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			else
+			{
+				_fnReDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( !oSettings.oFeatures.bFilter )
+			{
+				return;
+			}
+			
+			if ( bRegex === undefined || bRegex === null )
+			{
+				bRegex = false;
+			}
+			
+			if ( bSmart === undefined || bSmart === null )
+			{
+				bSmart = true;
+			}
+			
+			if ( bShowGlobal === undefined || bShowGlobal === null )
+			{
+				bShowGlobal = true;
+			}
+			
+			if ( bCaseInsensitive === undefined || bCaseInsensitive === null )
+			{
+				bCaseInsensitive = true;
+			}
+			
+			if ( iColumn === undefined || iColumn === null )
+			{
+				/* Global filter */
+				_fnFilterComplete( oSettings, {
+					"sSearch":sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				}, 1 );
+				
+				if ( bShowGlobal && oSettings.aanFeatures.f )
+				{
+					var n = oSettings.aanFeatures.f;
+					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+					{
+						// IE9 throws an 'unknown error' if document.activeElement is used
+						// inside an iframe or frame...
+						try {
+							if ( n[i]._DT_Input != document.activeElement )
+							{
+								$(n[i]._DT_Input).val( sInput );
+							}
+						}
+						catch ( e ) {
+							$(n[i]._DT_Input).val( sInput );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Single column filter */
+				$.extend( oSettings.aoPreSearchCols[ iColumn ], {
+					"sSearch": sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				} );
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the 
+		 * provided parameters.
+		 *  @param {int|node} [mRow] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [iCol] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( mRow, iCol )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( mRow !== undefined )
+			{
+				var iRow = mRow;
+				if ( typeof mRow === 'object' )
+				{
+					var sNode = mRow.nodeName.toLowerCase();
+					if (sNode === "tr" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow);
+					}
+					else if ( sNode === "td" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow.parentNode);
+						iCol = _fnNodeToColumnIndex( oSettings, iRow, mRow );
+					}
+				}
+		
+				if ( iCol !== undefined )
+				{
+					return _fnGetCellData( oSettings, iRow, iCol, '' );
+				}
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow]._aData : null;
+			}
+			return _fnGetDataMaster( oSettings );
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will 
+		 * typically want to use the '$' API method in preference to this as it is more 
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( iRow !== undefined ) {
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow].nTr : null;
+			}
+			return _fnGetTrNodes( oSettings );
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} nNode this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible), 
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *        
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *        
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *      
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( nNode )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var sNodeName = nNode.nodeName.toUpperCase();
+			
+			if ( sNodeName == "TR" )
+			{
+				return _fnNodeToDataIndex(oSettings, nNode);
+			}
+			else if ( sNodeName == "TD" || sNodeName == "TH" )
+			{
+				var iDataIndex = _fnNodeToDataIndex( oSettings, nNode.parentNode );
+				var iColumnIndex = _fnNodeToColumnIndex( oSettings, iDataIndex, nNode );
+				return [ iDataIndex, _fnColumnIndexToVisible(oSettings, iColumnIndex ), iColumnIndex ];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoOpenRows = oSettings.aoOpenRows;
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					return true;
+				}
+			}
+			return false;
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently 
+		 * on display on the page, with the HTML contents that is passed into the 
+		 * function. This can be used, for example, to ask for confirmation that a 
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		
+			/* Check that the row given is in the table */
+			var nTableRows = _fnGetTrNodes( oSettings );
+			if ( $.inArray(nTr, nTableRows) === -1 )
+			{
+				return;
+			}
+			
+			/* the old open one if there is one */
+			this.fnClose( nTr );
+			
+			var nNewRow = document.createElement("tr");
+			var nNewCell = document.createElement("td");
+			nNewRow.appendChild( nNewCell );
+			nNewCell.className = sClass;
+			nNewCell.colSpan = _fnVisbleColumns( oSettings );
+		
+			if (typeof mHtml === "string")
+			{
+				nNewCell.innerHTML = mHtml;
+			}
+			else
+			{
+				$(nNewCell).html( mHtml );
+			}
+		
+			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
+			var nTrs = $('tr', oSettings.nTBody);
+			if ( $.inArray(nTr, nTrs) != -1  )
+			{
+				$(nNewRow).insertAfter(nTr);
+			}
+			
+			oSettings.aoOpenRows.push( {
+				"nTr": nNewRow,
+				"nParent": nTr
+			} );
+			
+			return nNewRow;
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API 
+		 * function. With this function you can have a DataTables table go to the next, 
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnPageChange( oSettings, mAction );
+			_fnCalculateEnd( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen;
+			var aoColumns = oSettings.aoColumns;
+			var aoData = oSettings.aoData;
+			var nTd, bAppend, iBefore;
+			
+			/* No point in doing anything if we are requesting what is already true */
+			if ( aoColumns[iCol].bVisible == bShow )
+			{
+				return;
+			}
+			
+			/* Show the column */
+			if ( bShow )
+			{
+				var iInsert = 0;
+				for ( i=0 ; i<iCol ; i++ )
+				{
+					if ( aoColumns[i].bVisible )
+					{
+						iInsert++;
+					}
+				}
+				
+				/* Need to decide if we should use appendChild or insertBefore */
+				bAppend = (iInsert >= _fnVisbleColumns( oSettings ));
+		
+				/* Which coloumn should we be inserting before? */
+				if ( !bAppend )
+				{
+					for ( i=iCol ; i<aoColumns.length ; i++ )
+					{
+						if ( aoColumns[i].bVisible )
+						{
+							iBefore = i;
+							break;
+						}
+					}
+				}
+		
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						if ( bAppend )
+						{
+							aoData[i].nTr.appendChild( 
+								aoData[i]._anHidden[iCol]
+							);
+						}
+						else
+						{
+							aoData[i].nTr.insertBefore(
+								aoData[i]._anHidden[iCol], 
+								_fnGetTdNodes( oSettings, i )[iBefore] );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Remove a column from display */
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						nTd = _fnGetTdNodes( oSettings, i )[iCol];
+						aoData[i]._anHidden[iCol] = nTd;
+						nTd.parentNode.removeChild( nTd );
+					}
+				}
+			}
+		
+			/* Clear to set the visible flag */
+			aoColumns[iCol].bVisible = bShow;
+		
+			/* Redraw the header and footer based on the new column visibility */
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+			
+			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
+			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )
+			{
+				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );
+			}
+			
+			/* Do a redraw incase anything depending on the table columns needs it 
+			 * (built-in: scrolling) 
+			 */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnAdjustColumnSizing( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			_fnSaveState( oSettings );
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See 
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *      
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the 
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			oSettings.aaSorting = aaSort;
+			_fnSort( oSettings );
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			_fnSortAttachListener( _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ), nNode, iColumn,
+			 	fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update (not used of mData is an array or object)
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], 1, 0 ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, sDisplay;
+			var iRow = (typeof mRow === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mRow) : mRow;
+			
+			if ( $.isArray(mData) && iColumn === undefined )
+			{
+				/* Array update - update the whole row */
+				oSettings.aoData[iRow]._aData = mData.slice();
+				
+				/* Flag to the function that we are recursing */
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else if ( $.isPlainObject(mData) && iColumn === undefined )
+			{
+				/* Object update - update the whole row - assume the developer gets the object right */
+				oSettings.aoData[iRow]._aData = $.extend( true, {}, mData );
+		
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else
+			{
+				/* Individual cell update */
+				_fnSetCellData( oSettings, iRow, iColumn, mData );
+				sDisplay = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+				
+				var oCol = oSettings.aoColumns[iColumn];
+				if ( oCol.fnRender !== null )
+				{
+					sDisplay = _fnRender( oSettings, iRow, iColumn );
+					if ( oCol.bUseRendered )
+					{
+						_fnSetCellData( oSettings, iRow, iColumn, sDisplay );
+					}
+				}
+				
+				if ( oSettings.aoData[iRow].nTr !== null )
+				{
+					/* Do the actual HTML update */
+					_fnGetTdNodes( oSettings, iRow )[iColumn].innerHTML = sDisplay;
+				}
+			}
+			
+			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw
+			 * will rebuild the search array - however, the redraw might be disabled by the user)
+			 */
+			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );
+			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow(
+				oSettings, 
+				_fnGetRowData( oSettings, iRow, 'filter', _fnGetColumns( oSettings, 'bSearchable' ) )
+			);
+			
+			/* Perform pre-draw actions */
+			if ( bAction === undefined || bAction )
+			{
+				_fnAdjustColumnSizing( oSettings );
+			}
+			
+			/* Redraw the table */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = DataTable.ext.fnVersionCheck;
+		
+		
+		/*
+		 * This is really a good bit rubbish this method of exposing the internal methods
+		 * publicly... - To be fixed in 2.0 using methods on the prototype
+		 */
+		
+		
+		/**
+		 * Create a wrapper function for exporting an internal functions to an external API.
+		 *  @param {string} sFunc API function name
+		 *  @returns {function} wrapped function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnExternApiFunc (sFunc)
+		{
+			return function() {
+				var aArgs = [_fnSettingsFromNode(this[DataTable.ext.iApiIndex])].concat( 
+					Array.prototype.slice.call(arguments) );
+				return DataTable.ext.oApi[sFunc].apply( this, aArgs );
+			};
+		}
+		
+		
+		/**
+		 * Reference to internal functions for use by plug-in developers. Note that these
+		 * methods are references to internal functions and are considered to be private.
+		 * If you use these methods, be aware that they are liable to change between versions
+		 * (check the upgrade notes).
+		 *  @namespace
+		 */
+		this.oApi = {
+			"_fnExternApiFunc": _fnExternApiFunc,
+			"_fnInitialise": _fnInitialise,
+			"_fnInitComplete": _fnInitComplete,
+			"_fnLanguageCompat": _fnLanguageCompat,
+			"_fnAddColumn": _fnAddColumn,
+			"_fnColumnOptions": _fnColumnOptions,
+			"_fnAddData": _fnAddData,
+			"_fnCreateTr": _fnCreateTr,
+			"_fnGatherData": _fnGatherData,
+			"_fnBuildHead": _fnBuildHead,
+			"_fnDrawHead": _fnDrawHead,
+			"_fnDraw": _fnDraw,
+			"_fnReDraw": _fnReDraw,
+			"_fnAjaxUpdate": _fnAjaxUpdate,
+			"_fnAjaxParameters": _fnAjaxParameters,
+			"_fnAjaxUpdateDraw": _fnAjaxUpdateDraw,
+			"_fnServerParams": _fnServerParams,
+			"_fnAddOptionsHtml": _fnAddOptionsHtml,
+			"_fnFeatureHtmlTable": _fnFeatureHtmlTable,
+			"_fnScrollDraw": _fnScrollDraw,
+			"_fnAdjustColumnSizing": _fnAdjustColumnSizing,
+			"_fnFeatureHtmlFilter": _fnFeatureHtmlFilter,
+			"_fnFilterComplete": _fnFilterComplete,
+			"_fnFilterCustom": _fnFilterCustom,
+			"_fnFilterColumn": _fnFilterColumn,
+			"_fnFilter": _fnFilter,
+			"_fnBuildSearchArray": _fnBuildSearchArray,
+			"_fnBuildSearchRow": _fnBuildSearchRow,
+			"_fnFilterCreateSearch": _fnFilterCreateSearch,
+			"_fnDataToSearch": _fnDataToSearch,
+			"_fnSort": _fnSort,
+			"_fnSortAttachListener": _fnSortAttachListener,
+			"_fnSortingClasses": _fnSortingClasses,
+			"_fnFeatureHtmlPaginate": _fnFeatureHtmlPaginate,
+			"_fnPageChange": _fnPageChange,
+			"_fnFeatureHtmlInfo": _fnFeatureHtmlInfo,
+			"_fnUpdateInfo": _fnUpdateInfo,
+			"_fnFeatureHtmlLength": _fnFeatureHtmlLength,
+			"_fnFeatureHtmlProcessing": _fnFeatureHtmlProcessing,
+			"_fnProcessingDisplay": _fnProcessingDisplay,
+			"_fnVisibleToColumnIndex": _fnVisibleToColumnIndex,
+			"_fnColumnIndexToVisible": _fnColumnIndexToVisible,
+			"_fnNodeToDataIndex": _fnNodeToDataIndex,
+			"_fnVisbleColumns": _fnVisbleColumns,
+			"_fnCalculateEnd": _fnCalculateEnd,
+			"_fnConvertToWidth": _fnConvertToWidth,
+			"_fnCalculateColumnWidths": _fnCalculateColumnWidths,
+			"_fnScrollingWidthAdjust": _fnScrollingWidthAdjust,
+			"_fnGetWidestNode": _fnGetWidestNode,
+			"_fnGetMaxLenString": _fnGetMaxLenString,
+			"_fnStringToCss": _fnStringToCss,
+			"_fnDetectType": _fnDetectType,
+			"_fnSettingsFromNode": _fnSettingsFromNode,
+			"_fnGetDataMaster": _fnGetDataMaster,
+			"_fnGetTrNodes": _fnGetTrNodes,
+			"_fnGetTdNodes": _fnGetTdNodes,
+			"_fnEscapeRegex": _fnEscapeRegex,
+			"_fnDeleteIndex": _fnDeleteIndex,
+			"_fnReOrderIndex": _fnReOrderIndex,
+			"_fnColumnOrdering": _fnColumnOrdering,
+			"_fnLog": _fnLog,
+			"_fnClearTable": _fnClearTable,
+			"_fnSaveState": _fnSaveState,
+			"_fnLoadState": _fnLoadState,
+			"_fnCreateCookie": _fnCreateCookie,
+			"_fnReadCookie": _fnReadCookie,
+			"_fnDetectHeader": _fnDetectHeader,
+			"_fnGetUniqueThs": _fnGetUniqueThs,
+			"_fnScrollBarWidth": _fnScrollBarWidth,
+			"_fnApplyToChildren": _fnApplyToChildren,
+			"_fnMap": _fnMap,
+			"_fnGetRowData": _fnGetRowData,
+			"_fnGetCellData": _fnGetCellData,
+			"_fnSetCellData": _fnSetCellData,
+			"_fnGetObjectDataFn": _fnGetObjectDataFn,
+			"_fnSetObjectDataFn": _fnSetObjectDataFn,
+			"_fnApplyColumnDefs": _fnApplyColumnDefs,
+			"_fnBindAction": _fnBindAction,
+			"_fnExtend": _fnExtend,
+			"_fnCallbackReg": _fnCallbackReg,
+			"_fnCallbackFire": _fnCallbackFire,
+			"_fnJsonString": _fnJsonString,
+			"_fnRender": _fnRender,
+			"_fnNodeToColumnIndex": _fnNodeToColumnIndex,
+			"_fnInfoMacros": _fnInfoMacros,
+			"_fnBrowserDetect": _fnBrowserDetect,
+			"_fnGetColumns": _fnGetColumns
+		};
+		
+		$.extend( DataTable.ext.oApi, this.oApi );
+		
+		for ( var sFunc in DataTable.ext.oApi )
+		{
+			if ( sFunc )
+			{
+				this[sFunc] = _fnExternApiFunc(sFunc);
+			}
+		}
+		
+		
+		var _that = this;
+		this.each(function() {
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var bUsePassedData = false;
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, "Attempted to initialise DataTables on a node which is not a "+
+					"table: "+this.nodeName );
+				return;
+			}
+			
+			/* Check to see if we are re-initialising a table */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				/* Base check on table node */
+				if ( DataTable.settings[i].nTable == this )
+				{
+					if ( oInit === undefined || oInit.bRetrieve )
+					{
+						return DataTable.settings[i].oInstance;
+					}
+					else if ( oInit.bDestroy )
+					{
+						DataTable.settings[i].oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( DataTable.settings[i], 0, "Cannot reinitialise DataTable.\n\n"+
+							"To retrieve the DataTables object for this table, pass no arguments or see "+
+							"the docs for bRetrieve and bDestroy" );
+						return;
+					}
+				}
+				
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( DataTable.settings[i].sTableId == this.id )
+				{
+					DataTable.settings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._oExternConfig.iNextUnique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"nTable":        this,
+				"oApi":          _that.oApi,
+				"oInit":         oInit,
+				"sDestroyWidth": $(this).width(),
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			DataTable.settings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $(this).dataTable();
+			
+			/* Setting up the initialisation object */
+			if ( !oInit )
+			{
+				oInit = {};
+			}
+			
+			// Backwards compatibility, before we apply all the defaults
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			oInit = _fnExtend( $.extend(true, {}, DataTable.defaults), oInit );
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, "bPaginate" );
+			_fnMap( oSettings.oFeatures, oInit, "bLengthChange" );
+			_fnMap( oSettings.oFeatures, oInit, "bFilter" );
+			_fnMap( oSettings.oFeatures, oInit, "bSort" );
+			_fnMap( oSettings.oFeatures, oInit, "bInfo" );
+			_fnMap( oSettings.oFeatures, oInit, "bProcessing" );
+			_fnMap( oSettings.oFeatures, oInit, "bAutoWidth" );
+			_fnMap( oSettings.oFeatures, oInit, "bSortClasses" );
+			_fnMap( oSettings.oFeatures, oInit, "bServerSide" );
+			_fnMap( oSettings.oFeatures, oInit, "bDeferRender" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollX", "sX" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollXInner", "sXInner" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollY", "sY" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollInfinite", "bInfinite" );
+			_fnMap( oSettings.oScroll, oInit, "iScrollLoadGap", "iLoadGap" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollAutoCss", "bAutoCss" );
+			_fnMap( oSettings, oInit, "asStripeClasses" );
+			_fnMap( oSettings, oInit, "asStripClasses", "asStripeClasses" ); // legacy
+			_fnMap( oSettings, oInit, "fnServerData" );
+			_fnMap( oSettings, oInit, "fnFormatNumber" );
+			_fnMap( oSettings, oInit, "sServerMethod" );
+			_fnMap( oSettings, oInit, "aaSorting" );
+			_fnMap( oSettings, oInit, "aaSortingFixed" );
+			_fnMap( oSettings, oInit, "aLengthMenu" );
+			_fnMap( oSettings, oInit, "sPaginationType" );
+			_fnMap( oSettings, oInit, "sAjaxSource" );
+			_fnMap( oSettings, oInit, "sAjaxDataProp" );
+			_fnMap( oSettings, oInit, "iCookieDuration" );
+			_fnMap( oSettings, oInit, "sCookiePrefix" );
+			_fnMap( oSettings, oInit, "sDom" );
+			_fnMap( oSettings, oInit, "bSortCellsTop" );
+			_fnMap( oSettings, oInit, "iTabIndex" );
+			_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );
+			_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );
+			_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );
+			_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );
+			_fnMap( oSettings, oInit, "fnCookieCallback" );
+			_fnMap( oSettings, oInit, "fnStateLoad" );
+			_fnMap( oSettings, oInit, "fnStateSave" );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			if ( oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&
+				   oSettings.oFeatures.bSortClasses )
+			{
+				/* Enable sort classes for server-side processing. Safe to do it here, since server-side
+				 * processing must be enabled by the developer
+				 */
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'server_side_sort_classes' );
+			}
+			else if ( oSettings.oFeatures.bDeferRender )
+			{
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'defer_sort_classes' );
+			}
+			
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if 
+				 * you want to have multiple tables with multiple independent classes 
+				 */
+				$.extend( oSettings.oClasses, DataTable.ext.oJUIClasses );
+				
+				if ( oInit.sDom === DataTable.defaults.sDom && DataTable.defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			}
+			else
+			{
+				$.extend( oSettings.oClasses, DataTable.ext.oStdClasses );
+			}
+			$(this).addClass( oSettings.oClasses.sTable );
+			
+			/* Calculate the scroll bar width and cache it for use later on */
+			if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
+			}
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			/* Must be done after everything which can be overridden by a cookie! */
+			if ( oInit.bStateSave )
+			{
+				oSettings.oFeatures.bStateSave = true;
+				_fnLoadState( oSettings, oInit );
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			if ( oInit.aaData !== null )
+			{
+				bUsePassedData = true;
+			}
+			
+			/* Language definitions */
+			if ( oInit.oLanguage.sUrl !== "" )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that 
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
+				$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) {
+					_fnLanguageCompat( json );
+					$.extend( true, oSettings.oLanguage, oInit.oLanguage, json );
+					_fnInitialise( oSettings );
+				} );
+				bInitHandedOff = true;
+			}
+			else
+			{
+				$.extend( true, oSettings.oLanguage, oInit.oLanguage );
+			}
+			
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oSettings.oClasses.sStripeOdd,
+					oSettings.oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			iLen=oSettings.asStripeClasses.length;
+			oSettings.asDestroyStripes = [];
+			if (iLen)
+			{
+				var bStripeRemove = false;
+				var anRows = $(this).children('tbody').children('tr:lt(' + iLen + ')');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					if ( anRows.hasClass( oSettings.asStripeClasses[i] ) )
+					{
+						bStripeRemove = true;
+						
+						/* Store the classes which we are about to remove so they can be re-added on destroy */
+						oSettings.asDestroyStripes.push( oSettings.asStripeClasses[i] );
+					}
+				}
+				
+				if ( bStripeRemove )
+				{
+					anRows.removeClass( oSettings.asStripeClasses.join(' ') );
+				}
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				/* Short cut - use the loop to check if we have column visibility state to restore */
+				if ( oInit.saved_aoColumns !== undefined && oInit.saved_aoColumns.length == iLen )
+				{
+					if ( aoColumnsInit[i] === null )
+					{
+						aoColumnsInit[i] = {};
+					}
+					aoColumnsInit[i].bVisible = oInit.saved_aoColumns[i].bVisible;
+				}
+				
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			
+			/*
+			 * Sorting
+			 * Check the aaSorting array
+			 */
+			for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aaSorting[i][0] >= oSettings.aoColumns.length )
+				{
+					oSettings.aaSorting[i][0] = 0;
+				}
+				var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];
+				
+				/* Add a default sorting index */
+				if ( oSettings.aaSorting[i][2] === undefined )
+				{
+					oSettings.aaSorting[i][2] = 0;
+				}
+				
+				/* If aaSorting is not defined, then we use the first indicator in asSorting */
+				if ( oInit.aaSorting === undefined && oSettings.saved_aaSorting === undefined )
+				{
+					oSettings.aaSorting[i][1] = oColumn.asSorting[0];
+				}
+				
+				/* Set the current sorting index based on aoColumns.asSorting */
+				for ( j=0, jLen=oColumn.asSorting.length ; j<jLen ; j++ )
+				{
+					if ( oSettings.aaSorting[i][1] == oColumn.asSorting[j] )
+					{
+						oSettings.aaSorting[i][2] = j;
+						break;
+					}
+				}
+			}
+				
+			/* Do a first pass on the sorting classes (allows any size changes to be taken into
+			 * account, and also will apply sorting disabled classes if disabled
+			 */
+			_fnSortingClasses( oSettings );
+			
+			
+			/*
+			 * Final init
+			 * Cache the header, body and footer as required, creating them if needed
+			 */
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+			var captions = $(this).children('caption').each( function () {
+				this._captionSide = $(this).css('caption-side');
+			} );
+			
+			var thead = $(this).children('thead');
+			if ( thead.length === 0 )
+			{
+				thead = [ document.createElement( 'thead' ) ];
+				this.appendChild( thead[0] );
+			}
+			oSettings.nTHead = thead[0];
+			
+			var tbody = $(this).children('tbody');
+			if ( tbody.length === 0 )
+			{
+				tbody = [ document.createElement( 'tbody' ) ];
+				this.appendChild( tbody[0] );
+			}
+			oSettings.nTBody = tbody[0];
+			oSettings.nTBody.setAttribute( "role", "alert" );
+			oSettings.nTBody.setAttribute( "aria-live", "polite" );
+			oSettings.nTBody.setAttribute( "aria-relevant", "all" );
+			
+			var tfoot = $(this).children('tfoot');
+			if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") )
+			{
+				// If we are a scrolling table, and no footer has been given, then we need to create
+				// a tfoot element for the caption element to be appended to
+				tfoot = [ document.createElement( 'tfoot' ) ];
+				this.appendChild( tfoot[0] );
+			}
+			
+			if ( tfoot.length > 0 )
+			{
+				oSettings.nTFoot = tfoot[0];
+				_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+			}
+			
+			/* Check if there is data passing into the constructor */
+			if ( bUsePassedData )
+			{
+				for ( i=0 ; i<oInit.aaData.length ; i++ )
+				{
+					_fnAddData( oSettings, oInit.aaData[ i ] );
+				}
+			}
+			else
+			{
+				/* Grab the data from the page */
+				_fnGatherData( oSettings );
+			}
+			
+			/* Copy the data index array */
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			/* Initialisation complete - table can be drawn */
+			oSettings.bInitialised = true;
+			
+			/* Check if we need to initialise the table (it might not have been handed off to the
+			 * language processor)
+			 */
+			if ( bInitHandedOff === false )
+			{
+				_fnInitialise( oSettings );
+			}
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+	 * to ensure compatibility.
+	 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+	 *    formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+	 *    version, or false if this version of DataTales is not suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.fnVersionCheck( '1.9.0' ) );
+	 */
+	DataTable.fnVersionCheck = function( sVersion )
+	{
+		/* This is cheap, but effective */
+		var fnZPad = function (Zpad, count)
+		{
+			while(Zpad.length < count) {
+				Zpad += '0';
+			}
+			return Zpad;
+		};
+		var aThis = DataTable.ext.sVersion.split('.');
+		var aThat = sVersion.split('.');
+		var sThis = '', sThat = '';
+		
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ )
+		{
+			sThis += fnZPad( aThis[i], 3 );
+			sThat += fnZPad( aThat[i], 3 );
+		}
+		
+		return parseInt(sThis, 10) >= parseInt(sThat, 10);
+	};
+	
+	
+	/**
+	 * Check if a TABLE node is a DataTable table already or not.
+	 *  @param {node} nTable The TABLE node to check if it is a DataTable or not (note that other
+	 *    node types can be passed in, but will always return false).
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var ex = document.getElementById('example');
+	 *    if ( ! $.fn.DataTable.fnIsDataTable( ex ) ) {
+	 *      $(ex).dataTable();
+	 *    }
+	 */
+	DataTable.fnIsDataTable = function ( nTable )
+	{
+		var o = DataTable.settings;
+	
+		for ( var i=0 ; i<o.length ; i++ )
+		{
+			if ( o[i].nTable === nTable || o[i].nScrollHead === nTable || o[i].nScrollFoot === nTable )
+			{
+				return true;
+			}
+		}
+	
+		return false;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can select to
+	 * get only currently visible tables.
+	 *  @param {boolean} [bVisible=false] Flag to indicate if you want all (default) or 
+	 *    visible tables only.
+	 *  @returns {array} Array of TABLE nodes (not DataTable instances) which are DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var table = $.fn.dataTable.fnTables(true);
+	 *    if ( table.length > 0 ) {
+	 *      $(table).dataTable().fnAdjustColumnSizing();
+	 *    }
+	 */
+	DataTable.fnTables = function ( bVisible )
+	{
+		var out = [];
+	
+		jQuery.each( DataTable.settings, function (i, o) {
+			if ( !bVisible || (bVisible === true && $(o.nTable).is(':visible')) )
+			{
+				out.push( o.nTable );
+			}
+		} );
+	
+		return out;
+	};
+	
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+	 * e are optional
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.9.4";
+
+	/**
+	 * Private data store, containing all of the settings objects that are created for the
+	 * tables on a given page.
+	 * 
+	 * Note that the <i>DataTable.settings</i> object is aliased to <i>jQuery.fn.dataTableExt</i> 
+	 * through which it may be accessed and manipulated, or <i>jQuery.fn.dataTable.settings</i>.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has available
+	 * to it. These models define the objects that are used to hold the active state 
+	 * and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	/**
+	 * DataTables extension options and plug-ins. This namespace acts as a collection "area"
+	 * for plug-ins that can be used to extend the default DataTables behaviour - indeed many
+	 * of the build in methods use this method to provide their own capabilities (sorting methods
+	 * for example).
+	 * 
+	 * Note that this namespace is aliased to jQuery.fn.dataTableExt so it can be readily accessed
+	 * and modified by plug-ins.
+	 *  @namespace
+	 */
+	DataTable.models.ext = {
+		/**
+		 * Plug-in filtering functions - this method of filtering is complimentary to the default
+		 * type based filtering, and a lot more comprehensive as it allows you complete control
+		 * over the filtering logic. Each element in this array is a function (parameters
+		 * described below) that is called for every row in the table, and your logic decides if
+		 * it should be included in the filtered data set or not.
+		 *   <ul>
+		 *     <li>
+		 *       Function input parameters:
+		 *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *         <li>{array|object} Data for the row to be processed (same as the original format
+		 *           that was passed in as the data source, or an array from a DOM data source</li>
+		 *         <li>{int} Row index in aoData ({@link DataTable.models.oSettings.aoData}), which can
+		 *           be useful to retrieve the TR element if you need DOM interaction.</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{boolean} Include the row in the filtered result set (true) or not (false)</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom filtering being applied to the fourth column (i.e.
+		 *    // the aData[3] index) based on two input values from the end-user, matching the data in 
+		 *    // a certain range.
+		 *    $.fn.dataTableExt.afnFiltering.push(
+		 *      function( oSettings, aData, iDataIndex ) {
+		 *        var iMin = document.getElementById('min').value * 1;
+		 *        var iMax = document.getElementById('max').value * 1;
+		 *        var iVersion = aData[3] == "-" ? 0 : aData[3]*1;
+		 *        if ( iMin == "" && iMax == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin == "" && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && "" == iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		"afnFiltering": [],
+	
+	
+		/**
+		 * Plug-in sorting functions - this method of sorting is complimentary to the default type
+		 * based sorting that DataTables does automatically, allowing much greater control over the
+		 * the data that is being used to sort a column. This is useful if you want to do sorting
+		 * based on live data (for example the contents of an 'input' element) rather than just the
+		 * static string that DataTables knows of. The way these plug-ins work is that you create
+		 * an array of the values you wish to be sorted for the column in question and then return
+		 * that array. Which pre-sorting function is run here depends on the sSortDataType parameter
+		 * that is used for the column (if any). This is the corollary of <i>ofnSearch</i> for sort 
+		 * data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *         <li>{int} Target column index</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{array} Data for the column to be sorted upon</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type array
+		 *  @default []
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    // Updating the cached sorting information with user entered values in HTML input elements
+		 *    jQuery.fn.dataTableExt.afnSortData['dom-text'] = function ( oSettings, iColumn )
+		 *    {
+		 *      var aData = [];
+		 *      $( 'td:eq('+iColumn+') input', oSettings.oApi._fnGetTrNodes(oSettings) ).each( function () {
+		 *        aData.push( this.value );
+		 *      } );
+		 *      return aData;
+		 *    }
+		 */
+		"afnSortData": [],
+	
+	
+		/**
+		 * Feature plug-ins - This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are accessible through the sDom initialisation
+		 * option. As such, each feature plug-in must describe a function that is used to initialise
+		 * itself (fnInit), a character so the feature can be enabled by sDom (cFeature) and the name
+		 * of the feature (sFeature). Thus the objects attached to this method must provide:
+		 *   <ul>
+		 *     <li>{function} fnInit Initialisation of the plug-in
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>{node|null} The element which contains your feature. Note that the return
+		 *                may also be void if your plug-in does not require to inject any DOM elements 
+		 *                into DataTables control (sDom) - for example this might be useful when 
+		 *                developing a plug-in which allows table control via keyboard entry.</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </li>
+		 *     <li>{character} cFeature Character that will be matched in sDom - case sensitive</li>
+		 *     <li>{string} sFeature Feature name</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 * 
+		 *  @example
+		 *    // How TableTools initialises itself.
+		 *    $.fn.dataTableExt.aoFeatures.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T",
+		 *      "sFeature": "TableTools"
+		 *    } );
+		 */
+		"aoFeatures": [],
+	
+	
+		/**
+		 * Type detection plug-in functions - DataTables utilises types to define how sorting and
+		 * filtering behave, and types can be either  be defined by the developer (sType for the
+		 * column) or they can be automatically detected by the methods in this array. The functions
+		 * defined in the array are quite simple, taking a single parameter (the data to analyse) 
+		 * and returning the type if it is a known type, or null otherwise.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be analysed</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Data type detected, or null if unknown (and thus pass it
+		 *           on to the other type detection functions.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *  
+		 *  @example
+		 *    // Currency type detection plug-in:
+		 *    jQuery.fn.dataTableExt.aTypes.push(
+		 *      function ( sData ) {
+		 *        var sValidChars = "0123456789.-";
+		 *        var Char;
+		 *        
+		 *        // Check the numeric part
+		 *        for ( i=1 ; i<sData.length ; i++ ) {
+		 *          Char = sData.charAt(i); 
+		 *          if (sValidChars.indexOf(Char) == -1) {
+		 *            return null;
+		 *          }
+		 *        }
+		 *        
+		 *        // Check prefixed by currency
+		 *        if ( sData.charAt(0) == '$' || sData.charAt(0) == '&pound;' ) {
+		 *          return 'currency';
+		 *        }
+		 *        return null;
+		 *      }
+		 *    );
+		 */
+		"aTypes": [],
+	
+	
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, 
+		 * in order to ensure compatibility.
+		 *  @type function
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note 
+		 *    that the formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the 
+		 *    required version, or false if this version of DataTales is not suitable
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		"fnVersionCheck": DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @default 0
+		 */
+		"iApiIndex": 0,
+	
+	
+		/**
+		 * Pre-processing of filtering data plug-ins - When you assign the sType for a column
+		 * (or have it automatically detected for you by DataTables or a type detection plug-in), 
+		 * you will typically be using this for custom sorting, but it can also be used to provide 
+		 * custom filtering by allowing you to pre-processing the data and returning the data in
+		 * the format that should be filtered upon. This is done by adding functions this object 
+		 * with a parameter name which matches the sType for that target column. This is the
+		 * corollary of <i>afnSortData</i> for filtering data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be prepared for filtering</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Formatted string that will be used for the filtering.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 * 
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type object
+		 *  @default {}
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.ofnSearch['title-numeric'] = function ( sData ) {
+		 *      return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
+		 *    }
+		 */
+		"ofnSearch": {},
+	
+	
+		/**
+		 * Container for all private functions in DataTables so they can be exposed externally
+		 *  @type object
+		 *  @default {}
+		 */
+		"oApi": {},
+	
+	
+		/**
+		 * Storage for the various classes that DataTables uses
+		 *  @type object
+		 *  @default {}
+		 */
+		"oStdClasses": {},
+		
+	
+		/**
+		 * Storage for the various classes that DataTables uses - jQuery UI suitable
+		 *  @type object
+		 *  @default {}
+		 */
+		"oJUIClasses": {},
+	
+	
+		/**
+		 * Pagination plug-in methods - The style and controls of the pagination can significantly 
+		 * impact on how the end user interacts with the data in your table, and DataTables allows 
+		 * the addition of pagination controls by extending this object, which can then be enabled
+		 * through the <i>sPaginationType</i> initialisation parameter. Each pagination type that
+		 * is added is an object (the property name of which is what <i>sPaginationType</i> refers
+		 * to) that has two properties, both methods that are used by DataTables to update the
+		 * control's state.
+		 *   <ul>
+		 *     <li>
+		 *       fnInit -  Initialisation of the paging controls. Called only during initialisation 
+		 *         of the table. It is expected that this function will add the required DOM elements 
+		 *         to the page for the paging controls to work. The element pointer 
+		 *         'oSettings.aanFeatures.p' array is provided by DataTables to contain the paging 
+		 *         controls (note that this is a 2D array to allow for multiple instances of each 
+		 *         DataTables DOM element). It is suggested that you add the controls to this element 
+		 *         as children
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{node} Container into which the pagination controls must be inserted</li>
+		 *             <li>{function} Draw callback function - whenever the controls cause a page
+		 *               change, this method must be called to redraw the table.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *     <li>
+		 *       fnInit -  This function is called whenever the paging status of the table changes and is
+		 *         typically used to update classes and/or text of the paging controls to reflex the new 
+		 *         status.
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{function} Draw callback function - in case you need to redraw the table again
+		 *               or attach new event listeners</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.oPagination.four_button = {
+		 *      "fnInit": function ( oSettings, nPaging, fnCallbackDraw ) {
+		 *        nFirst = document.createElement( 'span' );
+		 *        nPrevious = document.createElement( 'span' );
+		 *        nNext = document.createElement( 'span' );
+		 *        nLast = document.createElement( 'span' );
+		 *        
+		 *        nFirst.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sFirst ) );
+		 *        nPrevious.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sPrevious ) );
+		 *        nNext.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sNext ) );
+		 *        nLast.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sLast ) );
+		 *        
+		 *        nFirst.className = "paginate_button first";
+		 *        nPrevious.className = "paginate_button previous";
+		 *        nNext.className="paginate_button next";
+		 *        nLast.className = "paginate_button last";
+		 *        
+		 *        nPaging.appendChild( nFirst );
+		 *        nPaging.appendChild( nPrevious );
+		 *        nPaging.appendChild( nNext );
+		 *        nPaging.appendChild( nLast );
+		 *        
+		 *        $(nFirst).click( function () {
+		 *          oSettings.oApi._fnPageChange( oSettings, "first" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nPrevious).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "previous" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nNext).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "next" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nLast).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "last" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nFirst).bind( 'selectstart', function () { return false; } );
+		 *        $(nPrevious).bind( 'selectstart', function () { return false; } );
+		 *        $(nNext).bind( 'selectstart', function () { return false; } );
+		 *        $(nLast).bind( 'selectstart', function () { return false; } );
+		 *      },
+		 *      
+		 *      "fnUpdate": function ( oSettings, fnCallbackDraw ) {
+		 *        if ( !oSettings.aanFeatures.p ) {
+		 *          return;
+		 *        }
+		 *        
+		 *        // Loop over each instance of the pager
+		 *        var an = oSettings.aanFeatures.p;
+		 *        for ( var i=0, iLen=an.length ; i<iLen ; i++ ) {
+		 *          var buttons = an[i].getElementsByTagName('span');
+		 *          if ( oSettings._iDisplayStart === 0 ) {
+		 *            buttons[0].className = "paginate_disabled_previous";
+		 *            buttons[1].className = "paginate_disabled_previous";
+		 *          }
+		 *          else {
+		 *            buttons[0].className = "paginate_enabled_previous";
+		 *            buttons[1].className = "paginate_enabled_previous";
+		 *          }
+		 *          
+		 *          if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) {
+		 *            buttons[2].className = "paginate_disabled_next";
+		 *            buttons[3].className = "paginate_disabled_next";
+		 *          }
+		 *          else {
+		 *            buttons[2].className = "paginate_enabled_next";
+		 *            buttons[3].className = "paginate_enabled_next";
+		 *          }
+		 *        }
+		 *      }
+		 *    };
+		 */
+		"oPagination": {},
+	
+	
+		/**
+		 * Sorting plug-in methods - Sorting in DataTables is based on the detected type of the
+		 * data column (you can add your own type detection functions, or override automatic 
+		 * detection using sType). With this specific type given to the column, DataTables will 
+		 * apply the required sort from the functions in the object. Each sort type must provide
+		 * two mandatory methods, one each for ascending and descending sorting, and can optionally
+		 * provide a pre-formatting method that will help speed up sorting by allowing DataTables
+		 * to pre-format the sort data only once (rather than every time the actual sort functions
+		 * are run). The two sorting functions are typical Javascript sort methods:
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data to compare to the second parameter</li>
+		 *         <li>{*} Data to compare to the first parameter</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{int} Sorting match: <0 if first parameter should be sorted lower than
+		 *           the second parameter, ===0 if the two parameters are equal and >0 if
+		 *           the first parameter should be sorted height than the second parameter.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Case-sensitive string sorting, with no pre-formatting method
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-case-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-case-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 *
+		 *  @example
+		 *    // Case-insensitive string sorting, with pre-formatting
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-pre": function(x) {
+		 *        return x.toLowerCase();
+		 *      },
+		 *      "string-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 */
+		"oSort": {},
+	
+	
+		/**
+		 * Version string for plug-ins to check compatibility. Allowed format is
+		 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+		 * e are optional
+		 *  @type string
+		 *  @default Version number
+		 */
+		"sVersion": DataTable.version,
+	
+	
+		/**
+		 * How should DataTables report an error. Can take the value 'alert' or 'throw'
+		 *  @type string
+		 *  @default alert
+		 */
+		"sErrMode": "alert",
+	
+	
+		/**
+		 * Store information for DataTables to access globally about other instances
+		 *  @namespace
+		 *  @private
+		 */
+		"_oExternConfig": {
+			/* int:iNextUnique - next unique number for an instance */
+			"iNextUnique": 0
+		}
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings 
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data 
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is 
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default []
+		 *  @private
+		 */
+		"_aSortData": [],
+	
+		/**
+		 * Array of TD elements that are cached for hidden rows, so they can be
+		 * reinserted into the table if a column is made visible again (or to act
+		 * as a store if a column is made hidden). Only hidden columns have a 
+		 * reference in the array. For non-hidden columns the value is either
+		 * undefined or null.
+		 *  @type array nodes
+		 *  @default []
+		 *  @private
+		 */
+		"_anHidden": [],
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": ""
+	};
+	
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults.columns} 
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+		
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+		
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+		
+		/**
+		 * <code>Deprecated</code> When using fnRender, you have two options for what 
+		 * to do with the data, and this property serves as the switch. Firstly, you 
+		 * can have the sorting and filtering use the rendered value (true - default), 
+		 * or you can have the sorting and filtering us the original value (false).
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @deprecated
+		 */
+		"bUseRendered": null,
+		
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+		
+		/**
+		 * Flag to indicate to the type detection method if the automatic type
+		 * detection should be used, or if a column type (sType) has been specified
+		 *  @type boolean
+		 *  @default true
+		 *  @private
+		 */
+		"_bAutoType": true,
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+		
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column 
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get - 
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+		
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @returns {string} The string you which to use in the display
+		 *  @default null
+		 *  @deprecated
+		 */
+		"fnRender": null,
+		
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b> 
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+		
+		/**
+		 * Property to read the value for the cells in the column from the data 
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+		
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+		
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+		
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used 
+		 * in DataTables as such, but can be used for plug-ins to reference the 
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+		
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+		
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+		
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+		
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+		
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+		
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+		
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+		
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation 
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which 
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine" },
+		 *          { "sTitle": "Browser" },
+		 *          { "sTitle": "Platform" },
+		 *          { "sTitle": "Version" },
+		 *          { "sTitle": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using an array of objects as a data source (mData)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine",   "mData": "engine" },
+		 *          { "sTitle": "Browser",  "mData": "browser" },
+		 *          { "sTitle": "Platform", "mData": "platform" },
+		 *          { "sTitle": "Version",  "mData": "version" },
+		 *          { "sTitle": "Grade",    "mData": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If sorting is enabled, then DataTables will perform a first pass sort on 
+		 * initialisation. You can define which column(s) the sort is performed upon, 
+		 * and the sorting direction, with this variable. The aaSorting array should 
+		 * contain an array for each column to be sorted initially containing the 
+		 * column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *    
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the aaSorting parameter, but 
+		 * cannot be overridden by user interaction with the table. What this means 
+		 * is that you could have a column (visible or hidden) which the sorting will 
+		 * always be forced on first - any sorting after that (from the user) will 
+		 * then be performed as required. This can be useful for grouping rows 
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSortingFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be 
+		 * either a 1D array of options which will be used for both the displayed 
+		 * option and the value, or a 2D array which will use the array in the first 
+		 * position as the value, and the array in the second position as the 
+		 * displayed options (useful for language strings such as 'All').
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aLengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 *  
+		 *  @example
+		 *    // Setting the default display length as well as length menu
+		 *    // This is likely to be wanted if you remove the '10' option which
+		 *    // is the iDisplayLength default.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 25,
+		 *        "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The aoColumns option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see 
+		 * {@link DataTable.defaults.columns}. Note that if you use aoColumns to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to aoColumns, aoColumnDefs allows you to target a specific 
+		 * column, multiple columns, or all columns, using the aTargets property of 
+		 * each object in the array. This allows great flexibility when creating 
+		 * tables, as the aoColumnDefs arrays can be of any length, targeting the 
+		 * columns you specifically want. aoColumnDefs may use any of the column 
+		 * options available: {@link DataTable.defaults.columns}, but it _must_
+		 * have aTargets defined in each object in the array. Values in the aTargets
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as oSearch, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size 
+		 * as the number of columns, and each element be an object with the parameters
+		 * "sSearch" and "bEscapeRegex" (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoSearchCols": [
+		 *          null,
+		 *          { "sSearch": "My filter" },
+		 *          null,
+		 *          { "sSearch": "^[0-9]", "bEscapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This 
+		 * array may be of any length, and DataTables will apply each class 
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the oClasses.sStripe*
+		 *    options</i>
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "asStripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using aoColumns.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bAutoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/arrays.txt",
+		 *        "bDeferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with 
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false,
+		 *        "bDestroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.sDom}.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information 
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bInfo": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bJQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (bPaginate).
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bLengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). bDestroy can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *    
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false,
+		 *        "bRetrieve": true
+		 *      } );
+		 *    }
+		 *    
+		 *    function tableActions ()
+		 *    {
+		 *      var oTable = initTable();
+		 *      // perform API operations with oTable 
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * Indicate if DataTables should be allowed to set the padding / margin
+		 * etc for the scrolling header elements or not. Typically you will want
+		 * this.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollAutoCss": false,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollAutoCss": true,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Enable infinite scrolling for DataTables (to be used in combination with
+		 * sScrollY). Infinite scrolling means that DataTables will continually load
+		 * data as a user scrolls through a table, which is very useful for large
+		 * dataset. This cannot be used with pagination, which is automatically
+		 * disabled. Note - the Scroller extra for DataTables is recommended in
+		 * in preference to this option.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollInfinite": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * sAjaxSource parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the "bSortable" option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSort": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bSortCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes 'sorting_1', 'sorting_2' and
+		 * 'sorting_3' to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSortClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled a cookie will be used to save
+		 * table display information such as pagination information, display length,
+		 * filtering and sorting. As such when the end user reloads the page the
+		 * display display will match what thy had previously set up.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * Customise the cookie and / or the parameters being stored when using
+		 * DataTables with state saving enabled. This function is called whenever
+		 * the cookie is modified, and it expects a fully formed cookie string to be
+		 * returned. Note that the data object passed in is a Javascript object which
+		 * must be converted to a string (JSON.stringify for example).
+		 *  @type function
+		 *  @param {string} sName Name of the cookie defined by DataTables
+		 *  @param {object} oData Data to be stored in the cookie
+		 *  @param {string} sExpires Cookie expires string
+		 *  @param {string} sPath Path of the cookie to set
+		 *  @returns {string} Cookie formatted string (which should be encoded by
+		 *    using encodeURIComponent())
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "fnCookieCallback": function (sName, oData, sExpires, sPath) {
+		 *          // Customise oData or sName or whatever else here
+		 *          return sName + "="+JSON.stringify(oData)+"; expires=" + sExpires +"; path=" + sPath;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCookieCallback": null,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDataIndex The index of this row in aoData
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnCreatedRow": function( nRow, aData, iDataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnDrawCallback": function( oSettings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' even.
+		 *  @type function
+		 *  @param {node} nFoot "TR" element for the footer
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the 
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the 
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFooterCallback": function( nFoot, aData, iStart, iEnd, aiDisplay ) {
+		 *          nFoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+iStart;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} iIn number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFormatNumber": function ( iIn ) {
+		 *          if ( iIn &lt; 1000 ) {
+		 *            return iIn;
+		 *          } else {
+		 *            var 
+		 *              s=(iIn+""), 
+		 *              a=s.split(""), out="", 
+		 *              iLen=s.length;
+		 *            
+		 *            for ( var i=0 ; i&lt;iLen ; i++ ) {
+		 *              if ( i%3 === 0 &amp;&amp; i !== 0 ) {
+		 *                out = "'"+out;
+		 *              }
+		 *              out = a[iLen-i-1]+out;
+		 *            }
+		 *          }
+		 *          return out;
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( iIn ) {
+			if ( iIn < 1000 )
+			{
+				// A small optimisation for what is likely to be the majority of use cases
+				return iIn;
+			}
+	
+			var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				if ( i%3 === 0 && i !== 0 )
+				{
+					out = this.oLanguage.sInfoThousands+out;
+				}
+				out = a[iLen-i-1]+out;
+			}
+			return out;
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} nHead "TR" element for the header
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnHeaderCallback": function( nHead, aData, iStart, iEnd, aiDisplay ) {
+		 *          nHead.getElementsByTagName('th')[0].innerHTML = "Displaying "+(iEnd-iStart)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} iStart Starting position in data for the draw
+		 *  @param {int} iEnd End position in data for the draw
+		 *  @param {int} iMax Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} iTotal Total number of rows in the data set, after filtering
+		 *  @param {string} sPre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "fnInfoCallback": function( oSettings, iStart, iEnd, iMax, iTotal, sPre ) {
+		 *        return iStart +" to "+ iEnd;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnInitComplete": function(oSettings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnPreDrawCallback": function( oSettings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDisplayIndex The display index for the current table draw
+		 *  @param {int} iDisplayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server ($.getJSON) so something more suitable for your
+		 * application. For example you could use POST data, or pull information from
+		 * a Gears or AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} sSource HTTP source to obtain the data from (sAjaxSource)
+		 *  @param {array} aoData A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} fnCallback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // POST data to server
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php",
+		 *        "fnServerData": function ( sSource, aoData, fnCallback, oSettings ) {
+		 *          oSettings.jqXHR = $.ajax( {
+		 *            "dataType": 'json', 
+		 *            "type": "POST", 
+		 *            "url": sSource, 
+		 *            "data": aoData, 
+		 *            "success": fnCallback
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerData": function ( sUrl, aoData, fnCallback, oSettings ) {
+			oSettings.jqXHR = $.ajax( {
+				"url":  sUrl,
+				"data": aoData,
+				"success": function (json) {
+					if ( json.sError ) {
+						oSettings.oApi._fnLog( oSettings, 0, json.sError );
+					}
+					
+					$(oSettings.oInstance).trigger('xhr', [oSettings, json]);
+					fnCallback( json );
+				},
+				"dataType": "json",
+				"cache": false,
+				"type": oSettings.sServerMethod,
+				"error": function (xhr, error, thrown) {
+					if ( error == "parsererror" ) {
+						oSettings.oApi._fnLog( oSettings, 0, "DataTables warning: JSON data from "+
+							"server could not be parsed. This is caused by a JSON formatting error." );
+					}
+				}
+			} );
+		},
+	
+	
+		/**
+		 * It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} aoData Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the aoData array passed in,
+		 *    as this is passed by reference.
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "fnServerParams": function ( aoData ) {
+		 *          aoData.push( { "name": "more_data", "value": "my_value" } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from its state saving
+		 * cookie, but you might wish to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @return {object} The DataTables state object to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoad": function (oSettings) {
+		 *          var o;
+		 *          
+		 *          // Send an Ajax request to the server to get the data. Note that
+		 *          // this is a synchronous request.
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "async": false,
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              o = json;
+		 *            }
+		 *          } );
+		 *          
+		 *          return o;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoad": function ( oSettings ) {
+			var sData = this.oApi._fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
+			var oData;
+	
+			try {
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON(sData) : eval( '('+sData+')' );
+			} catch (e) {
+				oData = null;
+			}
+	
+			return oData;
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for 
+		 * plug-in authors, you should use the 'stateLoadParams' event to load parameters for 
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that is to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that was loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoaded": function (oSettings, oData) {
+		 *          alert( 'Saved filter was: '+oData.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored - by default it will use a cookie, but you
+		 * might want to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSave": function (oSettings, oData) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": oData,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSave": function ( oSettings, oData ) {
+			this.oApi._fnCreateCookie( 
+				oSettings.sCookiePrefix+oSettings.sInstance, 
+				this.oApi._fnJsonString(oData), 
+				oSettings.iCookieDuration, 
+				oSettings.sCookiePrefix, 
+				oSettings.fnCookieCallback
+			);
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table 
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or 
+		 * other state properties or modification. Note that for plug-in authors, you should 
+		 * use the 'stateSaveParams' event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSaveParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration of the cookie which is used for storing session information. This
+		 * value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iCookieDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iCookieDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. iDeferLoading
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": 57
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": [ 57, 100 ],
+		 *        "oSearch": {
+		 *          "sSearch": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (bLengthChange) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * The scroll gap is the amount of scrolling that is left to go before
+		 * DataTables will load the next 'page' of data automatically. You typically
+		 * want a gap which is big enough that the scrolling will be smooth for the
+		 * user, while not so large that it will load more data than need.
+		 *  @type int
+		 *  @default 100
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px",
+		 *        "iScrollLoadGap": 50
+		 *      } );
+		 *    } );
+		 */
+		"iScrollLoadGap": 100,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a tabindex attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iTabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the two built-in pagination
+			 * control types ("two_button" and "full_numbers")
+			 *  @namespace
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sFirst": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+			
+			
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sLast": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+			
+			
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the 
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sNext": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+			
+			
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to  
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sPrevious": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+		
+			/**
+			 * This string is shown in preference to sZeroRecords when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of sZeroRecords will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sEmptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+		
+		
+			/**
+			 * This string gives information to the end user about the information that 
+			 * is current on display on the page. The _START_, _END_ and _TOTAL_ 
+			 * variables are all dynamically replaced as the table display updates, and 
+			 * can be freely moved or removed as the language requirements change.
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfo": "Got a total of _TOTAL_ entries to show (_START_ to _END_)"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+		
+		
+			/**
+			 * Display information string for when the table is empty. Typically the 
+			 * format of this string should match sInfo.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+		
+		
+			/**
+			 * When a user filters the information in a table, this string is appended 
+			 * to the information (sInfo) to give an idea of how strong the filtering 
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+		
+		
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the sInfo (sInfoEmpty and sInfoFiltered in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+		
+		
+			/**
+			 * DataTables has a build in number formatter (fnFormatNumber) which is used
+			 * to format large numbers that are used in the table information. By
+			 * default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoThousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoThousands": ",",
+		
+		
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+		
+		
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLoadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+		
+		
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sProcessing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+		
+		
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+		
+		
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sUrl": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+		
+		
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. sEmptyTable is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sZeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the "sSearch" parameter must be
+		 * defined, but all other parameters are optional. When "bRegex" is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When "bSmart"
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "oSearch": {"sSearch": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * By default DataTables will look for the property 'aaData' when obtaining
+		 * data from an Ajax source or for server-side processing - this parameter
+		 * allows that property to be changed. You can use Javascript dotted object
+		 * notation to get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default aaData
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // Get data from { "data": [...] }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data"
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Get data from { "data": { "inner": [...] } }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data.inner"
+		 *      } );
+		 *    } );
+		 */
+		"sAjaxDataProp": "aaData",
+	
+	
+		/**
+		 * You can instruct DataTables to load data from an external source using this
+		 * parameter (use aData if you want to pass data in you already have). Simply
+		 * provide a url a JSON object can be obtained from. This object must include
+		 * the parameter 'aaData' which is the data source for the table.
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sAjaxSource": "http://www.sprymedia.co.uk/dataTables/json.php"
+		 *      } );
+		 *    } )
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This parameter can be used to override the default prefix that DataTables
+		 * assigns to a cookie when state saving is enabled.
+		 *  @type string
+		 *  @default SpryMedia_DataTables_
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sCookiePrefix": "my_datatable_",
+		 *      } );
+		 *    } );
+		 */
+		"sCookiePrefix": "SpryMedia_DataTables_",
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:	
+		 *       <ul>
+		 *         <li>'l' - Length changing</li
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'&lt;' and '&gt;' - div elements</li>
+		 *         <li>'&lt;"class" and '&gt;' - div with a class</li>
+		 *         <li>'&lt;"#id" and '&gt;' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'&lt;"wrapper"flipt&gt;'</li>
+		 *         <li>'&lt;lf&lt;t&gt;ip&gt;'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when bJQueryUI is false)</i> <b>or</b> 
+		 *    <"H"lfr>t<"F"ip> <i>(when bJQueryUI is true)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sDom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * DataTables features two different built-in pagination interaction methods
+		 * ('two_button' or 'full_numbers') which present different page controls to
+		 * the end user. Further methods can be added using the API (see below).
+		 *  @type string
+		 *  @default two_button
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sPaginationType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "two_button",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a certain
+		 * layout, or you have a large number of columns in the table, you can enable
+		 * x-scrolling to show the table in a viewport, which can be scrolled. This
+		 * property can be any CSS unit, or a number (in which case it will be treated
+		 * as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "sScrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/post.php",
+		 *        "sServerMethod": "POST"
+		 *      } );
+		 *    } );
+		 */
+		"sServerMethod": "GET"
+	};
+	
+	
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.columns = {
+		/**
+		 * Allows a column's sorting to take multiple columns into account when 
+		 * doing a sort. For example first name / last name columns make sense to 
+		 * do a multi-column sort over the two columns.
+		 *  @type array
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "aDataSort": [ 0, 1 ], "aTargets": [ 0 ] },
+		 *          { "aDataSort": [ 1, 0 ], "aTargets": [ 1 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ], "aTargets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          { "aDataSort": [ 0, 1 ] },
+		 *          { "aDataSort": [ 1, 0 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ] },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+	
+	
+		/**
+		 * You can control the default sorting direction, and even alter the behaviour
+		 * of the sort handler (i.e. only allow ascending sorting etc) using this
+		 * parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "asSorting": [ "asc" ], "aTargets": [ 1 ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ], "aTargets": [ 2 ] },
+		 *          { "asSorting": [ "desc" ], "aTargets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          { "asSorting": [ "asc" ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ] },
+		 *          { "asSorting": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSearchable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSearchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable sorting on this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSortable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSortable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * <code>Deprecated</code> When using fnRender() for a column, you may wish 
+		 * to use the original data (before rendering) for sorting and filtering 
+		 * (the default is to used the rendered data that the user can see). This 
+		 * may be useful for dates etc.
+		 * 
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"bUseRendered": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bVisible": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bVisible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+		
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @param {int} iCol The column index for aoColumns
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [3],
+		 *          "fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
+		 *            if ( sData == "1.7" ) {
+		 *              $(nTd).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @param {object} o.mDataProp The data property used for this column
+		 *  @param {*}      val The current cell value
+		 *  @returns {string} The string you which to use in the display
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"fnRender": null,
+	
+	
+		/**
+		 * The column index (starting from 0!) that you wish a sort to be performed
+		 * upon when this column is selected for sorting. This can be used for sorting
+		 * on hidden columns for example.
+		 *  @type int
+		 *  @default -1 <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "iDataSort": 1, "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "iDataSort": 1 },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"iDataSort": -1,
+	
+	
+		/**
+		 * This parameter has been replaced by mData in DataTables to ensure naming
+		 * consistency. mDataProp can still be used, as there is backwards compatibility
+		 * in DataTables for this option, but it is strongly recommended that you use
+		 * mData in preference to mDataProp.
+		 *  @name DataTable.defaults.columns.mDataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any JSON data source property,
+		 * including deeply nested objects / properties. mData can be given in a
+		 * number of different ways which effect its behaviour:
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source.</li>
+		 *     <li>null - the sDefaultContent option will be used for the cell (null
+		 *       by default, so you will need to specify the default content you want -
+		 *       typically an empty string). This can be useful on generated columns such 
+		 *       as edit / delete action columns.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row</li>
+		 *         <li>{string} The type call data requested - this will be 'set' when
+		 *           setting data or 'filter', 'display', 'type', 'sort' or undefined when 
+		 *           gathering data. Note that when <i>undefined</i> is given for the type
+		 *           DataTables expects to get the raw data for the object back</li>
+		 *         <li>{*} Data to set when the second parameter is 'set'.</li>
+		 *       </ul>
+		 *       The return value from the function is not required when 'set' is the type
+		 *       of call, but otherwise the return is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *
+		 * Note that prior to DataTables 1.9.2 mData was called mDataProp. The name change
+		 * reflects the flexibility of this property and is consistent with the naming of
+		 * mRender. If 'mDataProp' is given, then it will still be used by DataTables, as
+		 * it automatically maps the old name to the new if required.
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Read table data from objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          { "mData": "platform.inner" },
+		 *          { "mData": "platform.details.0" },
+		 *          { "mData": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Using mData as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to mData and it is suggested that
+		 * when you want to manipulate data for display (including filtering, sorting etc)
+		 * but not altering the underlying data for the table, use this property. mData
+		 * can actually do everything this property can and more, but this parameter is
+		 * easier to use since there is no 'set' option. Like mData is can be given
+		 * in a number of different ways to effect its behaviour, with the addition of 
+		 * supporting array syntax for easy outputting of arrays (including arrays of
+		 * objects):
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source and also array brackets to indicate that the data reader should
+		 *       loop over the data source array. When characters are given between the array
+		 *       brackets, these characters are used to join the data source array together.
+		 *       For example: "accounts[, ].name" would result in a comma separated list with
+		 *       the 'name' value from the 'accounts' array of objects.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row (based on mData)</li>
+		 *         <li>{string} The type call data requested - this will be 'filter', 'display', 
+		 *           'type' or 'sort'.</li>
+		 *         <li>{array|object} The full data source for the row (not based on mData)</li>
+		 *       </ul>
+		 *       The return value from the function is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *  @type string|int|function|null
+		 *  @default null <i>Use mData</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          {
+		 *            "mData": "platform",
+		 *            "mRender": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *        {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": "download_link",
+		 *          "mRender": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "sCellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sClass": "my_class", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sClass": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this, and it is not documented on the 
+		 * general DataTables.net documentation
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *  @dtopt Columns
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "sContentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit",
+		 *            "aTargets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sName": "engine", "aTargets": [ 0 ] },
+		 *          { "sName": "browser", "aTargets": [ 1 ] },
+		 *          { "sName": "platform", "aTargets": [ 2 ] },
+		 *          { "sName": "version", "aTargets": [ 3 ] },
+		 *          { "sName": "grade", "aTargets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sName": "engine" },
+		 *          { "sName": "browser" },
+		 *          { "sName": "platform" },
+		 *          { "sName": "version" },
+		 *          { "sName": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the sorting which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to sorting. This allows sorting to occur on user editable
+		 * elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "sSortDataType": "dom-text", "aTargets": [ 2, 3 ] },
+		 *          { "sType": "numeric", "aTargets": [ 3 ] },
+		 *          { "sSortDataType": "dom-select", "aTargets": [ 4 ] },
+		 *          { "sSortDataType": "dom-checkbox", "aTargets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          null,
+		 *          { "sSortDataType": "dom-text" },
+		 *          { "sSortDataType": "dom-text", "sType": "numeric" },
+		 *          { "sSortDataType": "dom-select" },
+		 *          { "sSortDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the 
+		 *    original HTML table.</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sTitle": "My column title", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sTitle": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be sorted.
+		 * Four types (string, numeric, date and html (which will strip HTML tags
+		 * before sorting)) are currently available. Note that only date formats
+		 * understood by Javascript's Date() object will be accepted as type date. For
+		 * example: "Mar 26, 2008 5:03 PM". May take the values: 'string', 'numeric',
+		 * 'date' or 'html' (by default). Further types can be adding through
+		 * plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sType": "html", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sType": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables apples 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sWidth": "20%", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sWidth": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a 
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults} but this 
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant 
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+			
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+			
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+			
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+			
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+			
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+			
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+			
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+			
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+			
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+		
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * Indicate if DataTables should be allowed to set the padding / margin
+			 * etc for the scrolling header elements or not. Typically you will want
+			 * this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoCss": null,
+			
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+			
+			/**
+			 * Infinite scrolling enablement flag. Now deprecated in favour of
+			 * using the Scroller plug-in.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfinite": null,
+			
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+			
+			/**
+			 * Space (in pixels) between the bottom of the scrolling container and 
+			 * the bottom of the scrolling viewport before the next page is loaded
+			 * when using infinite scrolling.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type int
+			 */
+			"iLoadGap": null,
+			
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is 
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+			
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+			
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+		
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See 
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+		
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false
+		},
+		
+		/**
+		 * Array referencing the nodes which are used for the features. The 
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+		
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+		
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+		
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+		
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+		
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+		
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+		
+		/**
+		 * Search data array for regular expression searching
+		 *  @type array
+		 *  @default []
+		 */
+		"asDataSearch": [],
+		
+		/**
+		 * Store the applied global search information in case we want to force a 
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+		
+		/**
+		 * Store the applied search for each column - see 
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+		
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 *   <li>Index 2 - index of asSorting for this column</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+		
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array|null
+		 *  @default null
+		 */
+		"aaSortingFixed": null,
+		
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+		
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+		
+		/**
+		 * If restoring a table - we should restore its width 
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+		
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+		
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+		
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+		
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+		
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+		
+		/**
+		 * Callback functions for just before the table is redrawn. A return of 
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+		
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+		
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+		
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+		
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+		
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+		
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+		
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+		
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+		
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+		
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+		
+		/**
+		 * Indicate if when using server-side processing the loading of data 
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+		
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+		
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+		
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+		
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string 
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+		
+		/**
+		 * The cookie duration (for bStateSave) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iCookieDuration": 0,
+		
+		/**
+		 * The cookie name prefix.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sCookiePrefix": "",
+		
+		/**
+		 * Callback function for cookie creation.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 *  @default null
+		 */
+		"fnCookieCallback": null,
+		
+		/**
+		 * Array of callback functions for state saving. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object 
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+		
+		/**
+		 * Array of callback functions for state loading. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings 
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+		
+		/**
+		 * State that was loaded from the cookie. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+		
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+		
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case 
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+		
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+		
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering. 
+		 * This can be used for working with the XHR information in one of the 
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+		
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+		
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra 
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+		
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if 
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+		
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+		
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+		
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+		
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+		
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+		
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Paging end point - aiDisplay index. Use fnDisplayEnd rather than
+		 * this property to get the end point
+		 *  @type int
+		 *  @default 10
+		 *  @private
+		 */
+		"_iDisplayEnd": 10,
+		
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+		
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+		
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+		
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than 
+		 * one unique cell per column, if the top one (true) or bottom one (false) 
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+		
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+		
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+		
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsTotal, 10);
+			} else {
+				return this.aiDisplayMaster.length;
+			}
+		},
+		
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsDisplay, 10);
+			} else {
+				return this.aiDisplay.length;
+			}
+		},
+		
+		/**
+		 * Set the display end point - aiDisplay index
+		 *  @type function
+		 *  @todo Should do away with _iDisplayEnd and calculate it on-the-fly here
+		 */
+		"fnDisplayEnd": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {
+					return this._iDisplayStart+this.aiDisplay.length;
+				} else {
+					return Math.min( this._iDisplayStart+this._iDisplayLength, 
+						this._iRecordsDisplay );
+				}
+			} else {
+				return this._iDisplayEnd;
+			}
+		},
+		
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+		
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension options.
+	 * 
+	 * Note that the <i>DataTable.ext</i> object is available through
+	 * <i>jQuery.fn.dataTable.ext</i> where it may be accessed and manipulated. It is
+	 * also aliased to <i>jQuery.fn.dataTableExt</i> for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	DataTable.ext = $.extend( true, {}, DataTable.models.ext );
+	
+	$.extend( DataTable.ext.oStdClasses, {
+		"sTable": "dataTable",
+	
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "paginate_enabled_previous",
+		"sPagePrevDisabled": "paginate_disabled_previous",
+		"sPageNextEnabled": "paginate_enabled_next",
+		"sPageNextDisabled": "paginate_disabled_next",
+		"sPageJUINext": "",
+		"sPageJUIPrev": "",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "paginate_active",
+		"sPageButtonStaticDisabled": "paginate_button paginate_button_disabled",
+		"sPageFirst": "first",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last",
+		
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+		
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+		
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+		
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.oStdClasses, {
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",
+		"sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
+		"sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",
+		"sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
+		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
+		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "fg-button ui-button ui-state-default",
+		"sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageFirst": "first ui-corner-tl ui-corner-bl",
+		"sPageLast": "last ui-corner-tr ui-corner-br",
+		
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+		
+		/* Sorting */
+		"sSortAsc": "ui-state-default",
+		"sSortDesc": "ui-state-default",
+		"sSortable": "ui-state-default",
+		"sSortableAsc": "ui-state-default",
+		"sSortableDesc": "ui-state-default",
+		"sSortableNone": "ui-state-default",
+		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
+		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
+		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
+		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
+		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",
+		"sSortJUIWrapper": "DataTables_sort_wrapper",
+		"sSortIcon": "DataTables_sort_icon",
+		
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead ui-state-default",
+		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
+		
+		/* Misc */
+		"sFooterTH": "ui-state-default",
+		"sJUIHeader": "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",
+		"sJUIFooter": "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"
+	} );
+	
+	/*
+	 * Variable: oPagination
+	 * Purpose:  
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	$.extend( DataTable.ext.oPagination, {
+		/*
+		 * Variable: two_button
+		 * Purpose:  Standard two button (forward/back) pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"two_button": {
+			/*
+			 * Function: oPagination.two_button.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with forward/back buttons only
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				var sAppend = (!oSettings.bJUI) ?
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sPrevious+'</a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sNext+'</a>'
+					:
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUIPrev+'"></span></a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUINext+'"></span></a>';
+				$(nPaging).append( sAppend );
+				
+				var els = $('a', nPaging);
+				var nPrevious = els[0],
+					nNext = els[1];
+				
+				oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,     {action: "next"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nPrevious.id = oSettings.sTableId+'_previous';
+					nNext.id = oSettings.sTableId+'_next';
+	
+					nPrevious.setAttribute('aria-controls', oSettings.sTableId);
+					nNext.setAttribute('aria-controls', oSettings.sTableId);
+				}
+			},
+			
+			/*
+			 * Function: oPagination.two_button.fnUpdate
+			 * Purpose:  Update the two button pagination at the end of the draw
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var oClasses = oSettings.oClasses;
+				var an = oSettings.aanFeatures.p;
+				var nNode;
+	
+				/* Loop over each instance of the pager */
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i].firstChild;
+					if ( nNode )
+					{
+						/* Previous page */
+						nNode.className = ( oSettings._iDisplayStart === 0 ) ?
+						    oClasses.sPagePrevDisabled : oClasses.sPagePrevEnabled;
+						    
+						/* Next page */
+						nNode = nNode.nextSibling;
+						nNode.className = ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ?
+						    oClasses.sPageNextDisabled : oClasses.sPageNextEnabled;
+					}
+				}
+			}
+		},
+		
+		
+		/*
+		 * Variable: iFullNumbersShowPages
+		 * Purpose:  Change the number of pages which can be seen
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"iFullNumbersShowPages": 5,
+		
+		/*
+		 * Variable: full_numbers
+		 * Purpose:  Full numbers pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"full_numbers": {
+			/*
+			 * Function: oPagination.full_numbers.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with a list of the pages
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				$(nPaging).append(
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageFirst+'">'+oLang.sFirst+'</a>'+
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPagePrevious+'">'+oLang.sPrevious+'</a>'+
+					'<span></span>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageNext+'">'+oLang.sNext+'</a>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageLast+'">'+oLang.sLast+'</a>'
+				);
+				var els = $('a', nPaging);
+				var nFirst = els[0],
+					nPrev = els[1],
+					nNext = els[2],
+					nLast = els[3];
+				
+				oSettings.oApi._fnBindAction( nFirst, {action: "first"},    fnClickHandler );
+				oSettings.oApi._fnBindAction( nPrev,  {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,  {action: "next"},     fnClickHandler );
+				oSettings.oApi._fnBindAction( nLast,  {action: "last"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nFirst.id =oSettings.sTableId+'_first';
+					nPrev.id =oSettings.sTableId+'_previous';
+					nNext.id =oSettings.sTableId+'_next';
+					nLast.id =oSettings.sTableId+'_last';
+				}
+			},
+			
+			/*
+			 * Function: oPagination.full_numbers.fnUpdate
+			 * Purpose:  Update the list of page buttons shows
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var iPageCount = DataTable.ext.oPagination.iFullNumbersShowPages;
+				var iPageCountHalf = Math.floor(iPageCount / 2);
+				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
+				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
+				var sList = "";
+				var iStartButton, iEndButton, i, iLen;
+				var oClasses = oSettings.oClasses;
+				var anButtons, anStatic, nPaginateList, nNode;
+				var an = oSettings.aanFeatures.p;
+				var fnBind = function (j) {
+					oSettings.oApi._fnBindAction( this, {"page": j+iStartButton-1}, function(e) {
+						/* Use the information in the element to jump to the required page */
+						oSettings.oApi._fnPageChange( oSettings, e.data.page );
+						fnCallbackDraw( oSettings );
+						e.preventDefault();
+					} );
+				};
+				
+				/* Pages calculation */
+				if ( oSettings._iDisplayLength === -1 )
+				{
+					iStartButton = 1;
+					iEndButton = 1;
+					iCurrentPage = 1;
+				}
+				else if (iPages < iPageCount)
+				{
+					iStartButton = 1;
+					iEndButton = iPages;
+				}
+				else if (iCurrentPage <= iPageCountHalf)
+				{
+					iStartButton = 1;
+					iEndButton = iPageCount;
+				}
+				else if (iCurrentPage >= (iPages - iPageCountHalf))
+				{
+					iStartButton = iPages - iPageCount + 1;
+					iEndButton = iPages;
+				}
+				else
+				{
+					iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
+					iEndButton = iStartButton + iPageCount - 1;
+				}
+	
+				
+				/* Build the dynamic list */
+				for ( i=iStartButton ; i<=iEndButton ; i++ )
+				{
+					sList += (iCurrentPage !== i) ?
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+'">'+oSettings.fnFormatNumber(i)+'</a>' :
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButtonActive+'">'+oSettings.fnFormatNumber(i)+'</a>';
+				}
+				
+				/* Loop over each instance of the pager */
+				for ( i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i];
+					if ( !nNode.hasChildNodes() )
+					{
+						continue;
+					}
+					
+					/* Build up the dynamic list first - html and listeners */
+					$('span:eq(0)', nNode)
+						.html( sList )
+						.children('a').each( fnBind );
+					
+					/* Update the permanent button's classes */
+					anButtons = nNode.getElementsByTagName('a');
+					anStatic = [
+						anButtons[0], anButtons[1], 
+						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
+					];
+	
+					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
+					$([anStatic[0], anStatic[1]]).addClass( 
+						(iCurrentPage==1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+					$([anStatic[2], anStatic[3]]).addClass(
+						(iPages===0 || iCurrentPage===iPages || oSettings._iDisplayLength===-1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+				}
+			}
+		}
+	} );
+	
+	$.extend( DataTable.ext.oSort, {
+		/*
+		 * text sorting
+		 */
+		"string-pre": function ( a )
+		{
+			if ( typeof a != 'string' ) {
+				a = (a !== null && a.toString) ? a.toString() : '';
+			}
+			return a.toLowerCase();
+		},
+	
+		"string-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"string-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * html sorting (ignore html tags)
+		 */
+		"html-pre": function ( a )
+		{
+			return a.replace( /<.*?>/g, "" ).toLowerCase();
+		},
+		
+		"html-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"html-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * date sorting
+		 */
+		"date-pre": function ( a )
+		{
+			var x = Date.parse( a );
+			
+			if ( isNaN(x) || x==="" )
+			{
+				x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			return x;
+		},
+	
+		"date-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"date-desc": function ( x, y )
+		{
+			return y - x;
+		},
+		
+		
+		/*
+		 * numerical sorting
+		 */
+		"numeric-pre": function ( a )
+		{
+			return (a=="-" || a==="") ? 0 : a*1;
+		},
+	
+		"numeric-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"numeric-desc": function ( x, y )
+		{
+			return y - x;
+		}
+	} );
+	
+	
+	$.extend( DataTable.ext.aTypes, [
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is numeric
+		 * Returns:  string:'numeric' or null
+		 * Inputs:   mixed:sText - string to check
+		 */
+		function ( sData )
+		{
+			/* Allow zero length strings as a number */
+			if ( typeof sData === 'number' )
+			{
+				return 'numeric';
+			}
+			else if ( typeof sData !== 'string' )
+			{
+				return null;
+			}
+			
+			var sValidFirstChars = "0123456789-";
+			var sValidChars = "0123456789.";
+			var Char;
+			var bDecimal = false;
+			
+			/* Check for a valid first char (no period and allow negatives) */
+			Char = sData.charAt(0); 
+			if (sValidFirstChars.indexOf(Char) == -1) 
+			{
+				return null;
+			}
+			
+			/* Check all the other characters are valid */
+			for ( var i=1 ; i<sData.length ; i++ ) 
+			{
+				Char = sData.charAt(i); 
+				if (sValidChars.indexOf(Char) == -1) 
+				{
+					return null;
+				}
+				
+				/* Only allowed one decimal place... */
+				if ( Char == "." )
+				{
+					if ( bDecimal )
+					{
+						return null;
+					}
+					bDecimal = true;
+				}
+			}
+			
+			return 'numeric';
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is actually a formatted date
+		 * Returns:  string:'date' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			var iParse = Date.parse(sData);
+			if ( (iParse !== null && !isNaN(iParse)) || (typeof sData === 'string' && sData.length === 0) )
+			{
+				return 'date';
+			}
+			return null;
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string should be treated as an HTML string
+		 * Returns:  string:'html' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			if ( typeof sData === 'string' && sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )
+			{
+				return 'html';
+			}
+			return null;
+		}
+	] );
+	
+
+	// jQuery aliases
+	$.fn.DataTable = DataTable;
+	$.fn.dataTable = DataTable;
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same point as
+	 * fnDrawCallback. This may be useful for binding events or performing calculations when
+	 * the table is altered at all.
+	 *  @name DataTable#draw
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Filter event, fired when the filtering applied to the table (using the build in global
+	 * global filter, or column filters) is altered.
+	 *  @name DataTable#filter
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Sort event, fired when the sorting applied to the table is altered.
+	 *  @name DataTable#sort
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully drawn,
+	 * including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save is required.
+	 * This method allows modification of the state saving object prior to actually doing the
+	 * save, including addition or other state properties (for plug-ins) or modification
+	 * of a DataTables core property.
+	 *  @name DataTable#stateSaveParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored data, but
+	 * prior to the settings object being modified by the saved state - allowing modification
+	 * of the saved state is required or loading of state for a plug-in.
+	 *  @name DataTable#stateLoadParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and the settings
+	 * object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing (be it,
+	 * sort, filter or anything else). Can be used to indicate to the end user that
+	 * there is something happening, or that something has finished.
+	 *  @name DataTable#processing
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a request to 
+	 * made to the server for new data (note that this trigger is called in fnServerData,
+	 * if you override fnServerData and which to use this event, you need to trigger it in
+	 * you success function).
+	 *  @name DataTable#xhr
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy or passing
+	 * the bDestroy:true parameter in the initialisation object. This can be used to remove
+	 * bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+}));
+
+}(window, document));
+
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.min.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.min.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/jquery.dataTables.min.js	(revision 16045)
@@ -0,0 +1,155 @@
+/*
+ * File:        jquery.dataTables.min.js
+ * Version:     1.9.4
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Info:        www.datatables.net
+ * 
+ * Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file 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 license files for details.
+ */
+(function(X,l,n){var L=function(h){var j=function(e){function o(a,b){var c=j.defaults.columns,d=a.aoColumns.length,c=h.extend({},j.models.oColumn,c,{sSortingClass:a.oClasses.sSortable,sSortingClassJUI:a.oClasses.sSortJUI,nTh:b?b:l.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.oDefaults:d});a.aoColumns.push(c);if(a.aoPreSearchCols[d]===n||null===a.aoPreSearchCols[d])a.aoPreSearchCols[d]=h.extend({},j.models.oSearch);else if(c=a.aoPreSearchCols[d],
+c.bRegex===n&&(c.bRegex=!0),c.bSmart===n&&(c.bSmart=!0),c.bCaseInsensitive===n)c.bCaseInsensitive=!0;m(a,d,null)}function m(a,b,c){var d=a.aoColumns[b];c!==n&&null!==c&&(c.mDataProp&&!c.mData&&(c.mData=c.mDataProp),c.sType!==n&&(d.sType=c.sType,d._bAutoType=!1),h.extend(d,c),p(d,c,"sWidth","sWidthOrig"),c.iDataSort!==n&&(d.aDataSort=[c.iDataSort]),p(d,c,"aDataSort"));var i=d.mRender?Q(d.mRender):null,f=Q(d.mData);d.fnGetData=function(a,b){var c=f(a,b);return d.mRender&&b&&""!==b?i(c,b,a):c};d.fnSetData=
+L(d.mData);a.oFeatures.bSort||(d.bSortable=!1);!d.bSortable||-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableNone,d.sSortingClassJUI=""):-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortable,d.sSortingClassJUI=a.oClasses.sSortJUI):-1!=h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableAsc,d.sSortingClassJUI=a.oClasses.sSortJUIAscAllowed):-1==
+h.inArray("asc",d.asSorting)&&-1!=h.inArray("desc",d.asSorting)&&(d.sSortingClass=a.oClasses.sSortableDesc,d.sSortingClassJUI=a.oClasses.sSortJUIDescAllowed)}function k(a){if(!1===a.oFeatures.bAutoWidth)return!1;da(a);for(var b=0,c=a.aoColumns.length;b<c;b++)a.aoColumns[b].nTh.style.width=a.aoColumns[b].sWidth}function G(a,b){var c=r(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function R(a,b){var c=r(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function t(a){return r(a,"bVisible").length}
+function r(a,b){var c=[];h.map(a.aoColumns,function(a,i){a[b]&&c.push(i)});return c}function B(a){for(var b=j.ext.aTypes,c=b.length,d=0;d<c;d++){var i=b[d](a);if(null!==i)return i}return"string"}function u(a,b){for(var c=b.split(","),d=[],i=0,f=a.aoColumns.length;i<f;i++)for(var g=0;g<f;g++)if(a.aoColumns[i].sName==c[g]){d.push(g);break}return d}function M(a){for(var b="",c=0,d=a.aoColumns.length;c<d;c++)b+=a.aoColumns[c].sName+",";return b.length==d?"":b.slice(0,-1)}function ta(a,b,c,d){var i,f,
+g,e,w;if(b)for(i=b.length-1;0<=i;i--){var j=b[i].aTargets;h.isArray(j)||D(a,1,"aTargets must be an array of targets, not a "+typeof j);f=0;for(g=j.length;f<g;f++)if("number"===typeof j[f]&&0<=j[f]){for(;a.aoColumns.length<=j[f];)o(a);d(j[f],b[i])}else if("number"===typeof j[f]&&0>j[f])d(a.aoColumns.length+j[f],b[i]);else if("string"===typeof j[f]){e=0;for(w=a.aoColumns.length;e<w;e++)("_all"==j[f]||h(a.aoColumns[e].nTh).hasClass(j[f]))&&d(e,b[i])}}if(c){i=0;for(a=c.length;i<a;i++)d(i,c[i])}}function H(a,
+b){var c;c=h.isArray(b)?b.slice():h.extend(!0,{},b);var d=a.aoData.length,i=h.extend(!0,{},j.models.oRow);i._aData=c;a.aoData.push(i);for(var f,i=0,g=a.aoColumns.length;i<g;i++)c=a.aoColumns[i],"function"===typeof c.fnRender&&c.bUseRendered&&null!==c.mData?F(a,d,i,S(a,d,i)):F(a,d,i,v(a,d,i)),c._bAutoType&&"string"!=c.sType&&(f=v(a,d,i,"type"),null!==f&&""!==f&&(f=B(f),null===c.sType?c.sType=f:c.sType!=f&&"html"!=c.sType&&(c.sType="string")));a.aiDisplayMaster.push(d);a.oFeatures.bDeferRender||ea(a,
+d);return d}function ua(a){var b,c,d,i,f,g,e;if(a.bDeferLoading||null===a.sAjaxSource)for(b=a.nTBody.firstChild;b;){if("TR"==b.nodeName.toUpperCase()){c=a.aoData.length;b._DT_RowIndex=c;a.aoData.push(h.extend(!0,{},j.models.oRow,{nTr:b}));a.aiDisplayMaster.push(c);f=b.firstChild;for(d=0;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)F(a,c,d,h.trim(f.innerHTML)),d++;f=f.nextSibling}}b=b.nextSibling}i=T(a);d=[];b=0;for(c=i.length;b<c;b++)for(f=i[b].firstChild;f;)g=f.nodeName.toUpperCase(),("TD"==
+g||"TH"==g)&&d.push(f),f=f.nextSibling;c=0;for(i=a.aoColumns.length;c<i;c++){e=a.aoColumns[c];null===e.sTitle&&(e.sTitle=e.nTh.innerHTML);var w=e._bAutoType,o="function"===typeof e.fnRender,k=null!==e.sClass,n=e.bVisible,m,p;if(w||o||k||!n){g=0;for(b=a.aoData.length;g<b;g++)f=a.aoData[g],m=d[g*i+c],w&&"string"!=e.sType&&(p=v(a,g,c,"type"),""!==p&&(p=B(p),null===e.sType?e.sType=p:e.sType!=p&&"html"!=e.sType&&(e.sType="string"))),e.mRender?m.innerHTML=v(a,g,c,"display"):e.mData!==c&&(m.innerHTML=v(a,
+g,c,"display")),o&&(p=S(a,g,c),m.innerHTML=p,e.bUseRendered&&F(a,g,c,p)),k&&(m.className+=" "+e.sClass),n?f._anHidden[c]=null:(f._anHidden[c]=m,m.parentNode.removeChild(m)),e.fnCreatedCell&&e.fnCreatedCell.call(a.oInstance,m,v(a,g,c,"display"),f._aData,g,c)}}if(0!==a.aoRowCreatedCallback.length){b=0;for(c=a.aoData.length;b<c;b++)f=a.aoData[b],A(a,"aoRowCreatedCallback",null,[f.nTr,f._aData,b])}}function I(a,b){return b._DT_RowIndex!==n?b._DT_RowIndex:null}function fa(a,b,c){for(var b=J(a,b),d=0,a=
+a.aoColumns.length;d<a;d++)if(b[d]===c)return d;return-1}function Y(a,b,c,d){for(var i=[],f=0,g=d.length;f<g;f++)i.push(v(a,b,d[f],c));return i}function v(a,b,c,d){var i=a.aoColumns[c];if((c=i.fnGetData(a.aoData[b]._aData,d))===n)return a.iDrawError!=a.iDraw&&null===i.sDefaultContent&&(D(a,0,"Requested unknown parameter "+("function"==typeof i.mData?"{mData function}":"'"+i.mData+"'")+" from the data source for row "+b),a.iDrawError=a.iDraw),i.sDefaultContent;if(null===c&&null!==i.sDefaultContent)c=
+i.sDefaultContent;else if("function"===typeof c)return c();return"display"==d&&null===c?"":c}function F(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d)}function Q(a){if(null===a)return function(){return null};if("function"===typeof a)return function(b,d,i){return a(b,d,i)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var f=i.split("."),g;if(""!==i){var e=0;for(g=f.length;e<g;e++){if(i=f[e].match(U)){f[e]=f[e].replace(U,"");""!==f[e]&&(a=a[f[e]]);
+g=[];f.splice(0,e+1);for(var f=f.join("."),e=0,h=a.length;e<h;e++)g.push(b(a[e],d,f));a=i[0].substring(1,i[0].length-1);a=""===a?g:g.join(a);break}if(null===a||a[f[e]]===n)return n;a=a[f[e]]}}return a};return function(c,d){return b(c,d,a)}}return function(b){return b[a]}}function L(a){if(null===a)return function(){};if("function"===typeof a)return function(b,d){a(b,"set",d)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var i=i.split("."),f,g,e=0;for(g=
+i.length-1;e<g;e++){if(f=i[e].match(U)){i[e]=i[e].replace(U,"");a[i[e]]=[];f=i.slice();f.splice(0,e+1);g=f.join(".");for(var h=0,j=d.length;h<j;h++)f={},b(f,d[h],g),a[i[e]].push(f);return}if(null===a[i[e]]||a[i[e]]===n)a[i[e]]={};a=a[i[e]]}a[i[i.length-1].replace(U,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Z(a){for(var b=[],c=a.aoData.length,d=0;d<c;d++)b.push(a.aoData[d]._aData);return b}function ga(a){a.aoData.splice(0,a.aoData.length);a.aiDisplayMaster.splice(0,
+a.aiDisplayMaster.length);a.aiDisplay.splice(0,a.aiDisplay.length);y(a)}function ha(a,b){for(var c=-1,d=0,i=a.length;d<i;d++)a[d]==b?c=d:a[d]>b&&a[d]--; -1!=c&&a.splice(c,1)}function S(a,b,c){var d=a.aoColumns[c];return d.fnRender({iDataRow:b,iDataColumn:c,oSettings:a,aData:a.aoData[b]._aData,mDataProp:d.mData},v(a,b,c,"display"))}function ea(a,b){var c=a.aoData[b],d;if(null===c.nTr){c.nTr=l.createElement("tr");c.nTr._DT_RowIndex=b;c._aData.DT_RowId&&(c.nTr.id=c._aData.DT_RowId);c._aData.DT_RowClass&&
+(c.nTr.className=c._aData.DT_RowClass);for(var i=0,f=a.aoColumns.length;i<f;i++){var g=a.aoColumns[i];d=l.createElement(g.sCellType);d.innerHTML="function"===typeof g.fnRender&&(!g.bUseRendered||null===g.mData)?S(a,b,i):v(a,b,i,"display");null!==g.sClass&&(d.className=g.sClass);g.bVisible?(c.nTr.appendChild(d),c._anHidden[i]=null):c._anHidden[i]=d;g.fnCreatedCell&&g.fnCreatedCell.call(a.oInstance,d,v(a,b,i,"display"),c._aData,b,i)}A(a,"aoRowCreatedCallback",null,[c.nTr,c._aData,b])}}function va(a){var b,
+c,d;if(0!==h("th, td",a.nTHead).length){b=0;for(d=a.aoColumns.length;b<d;b++)if(c=a.aoColumns[b].nTh,c.setAttribute("role","columnheader"),a.aoColumns[b].bSortable&&(c.setAttribute("tabindex",a.iTabIndex),c.setAttribute("aria-controls",a.sTableId)),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),a.aoColumns[b].sTitle!=c.innerHTML)c.innerHTML=a.aoColumns[b].sTitle}else{var i=l.createElement("tr");b=0;for(d=a.aoColumns.length;b<d;b++)c=a.aoColumns[b].nTh,c.innerHTML=a.aoColumns[b].sTitle,
+c.setAttribute("tabindex","0"),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),i.appendChild(c);h(a.nTHead).html("")[0].appendChild(i);V(a.aoHeader,a.nTHead)}h(a.nTHead).children("tr").attr("role","row");if(a.bJUI){b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;i=l.createElement("div");i.className=a.oClasses.sSortJUIWrapper;h(c).contents().appendTo(i);var f=l.createElement("span");f.className=a.oClasses.sSortIcon;i.appendChild(f);c.appendChild(i)}}if(a.oFeatures.bSort)for(b=
+0;b<a.aoColumns.length;b++)!1!==a.aoColumns[b].bSortable?ia(a,a.aoColumns[b].nTh,b):h(a.aoColumns[b].nTh).addClass(a.oClasses.sSortableNone);""!==a.oClasses.sFooterTH&&h(a.nTFoot).children("tr").children("th").addClass(a.oClasses.sFooterTH);if(null!==a.nTFoot){c=N(a,null,a.aoFooter);b=0;for(d=a.aoColumns.length;b<d;b++)c[b]&&(a.aoColumns[b].nTf=c[b],a.aoColumns[b].sClass&&h(c[b]).addClass(a.aoColumns[b].sClass))}}function W(a,b,c){var d,i,f,g=[],e=[],h=a.aoColumns.length,j;c===n&&(c=!1);d=0;for(i=
+b.length;d<i;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=h-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);e.push([])}d=0;for(i=g.length;d<i;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(j=h=1,e[d][f]===n){a.appendChild(g[d][f].cell);for(e[d][f]=1;g[d+h]!==n&&g[d][f].cell==g[d+h][f].cell;)e[d+h][f]=1,h++;for(;g[d][f+j]!==n&&g[d][f].cell==g[d][f+j].cell;){for(c=0;c<h;c++)e[d+c][f+j]=1;j++}g[d][f].cell.rowSpan=h;g[d][f].cell.colSpan=j}}}function x(a){var b=
+A(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))E(a,!1);else{var c,d,b=[],i=0,f=a.asStripeClasses.length;c=a.aoOpenRows.length;a.bDrawing=!0;a.iInitDisplayStart!==n&&-1!=a.iInitDisplayStart&&(a._iDisplayStart=a.oFeatures.bServerSide?a.iInitDisplayStart:a.iInitDisplayStart>=a.fnRecordsDisplay()?0:a.iInitDisplayStart,a.iInitDisplayStart=-1,y(a));if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++;else if(a.oFeatures.bServerSide){if(!a.bDestroying&&!wa(a))return}else a.iDraw++;if(0!==a.aiDisplay.length){var g=
+a._iDisplayStart;d=a._iDisplayEnd;a.oFeatures.bServerSide&&(g=0,d=a.aoData.length);for(;g<d;g++){var e=a.aoData[a.aiDisplay[g]];null===e.nTr&&ea(a,a.aiDisplay[g]);var j=e.nTr;if(0!==f){var o=a.asStripeClasses[i%f];e._sRowStripe!=o&&(h(j).removeClass(e._sRowStripe).addClass(o),e._sRowStripe=o)}A(a,"aoRowCallback",null,[j,a.aoData[a.aiDisplay[g]]._aData,i,g]);b.push(j);i++;if(0!==c)for(e=0;e<c;e++)if(j==a.aoOpenRows[e].nParent){b.push(a.aoOpenRows[e].nTr);break}}}else b[0]=l.createElement("tr"),a.asStripeClasses[0]&&
+(b[0].className=a.asStripeClasses[0]),c=a.oLanguage,f=c.sZeroRecords,1==a.iDraw&&null!==a.sAjaxSource&&!a.oFeatures.bServerSide?f=c.sLoadingRecords:c.sEmptyTable&&0===a.fnRecordsTotal()&&(f=c.sEmptyTable),c=l.createElement("td"),c.setAttribute("valign","top"),c.colSpan=t(a),c.className=a.oClasses.sRowEmpty,c.innerHTML=ja(a,f),b[i].appendChild(c);A(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);A(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],
+Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);i=l.createDocumentFragment();c=l.createDocumentFragment();if(a.nTBody){f=a.nTBody.parentNode;c.appendChild(a.nTBody);if(!a.oScroll.bInfinite||!a._bInitComplete||a.bSorted||a.bFiltered)for(;c=a.nTBody.firstChild;)a.nTBody.removeChild(c);c=0;for(d=b.length;c<d;c++)i.appendChild(b[c]);a.nTBody.appendChild(i);null!==f&&f.appendChild(a.nTBody)}A(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1;a.oFeatures.bServerSide&&(E(a,!1),
+a._bInitComplete||$(a))}}function aa(a){a.oFeatures.bSort?O(a,a.oPreviousSearch):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(y(a),x(a))}function xa(a){var b=h("<div></div>")[0];a.nTable.parentNode.insertBefore(b,a.nTable);a.nTableWrapper=h('<div id="'+a.sTableId+'_wrapper" class="'+a.oClasses.sWrapper+'" role="grid"></div>')[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var c=a.nTableWrapper,d=a.sDom.split(""),i,f,g,e,w,o,k,m=0;m<d.length;m++){f=0;g=d[m];if("<"==g){e=h("<div></div>")[0];w=d[m+
+1];if("'"==w||'"'==w){o="";for(k=2;d[m+k]!=w;)o+=d[m+k],k++;"H"==o?o=a.oClasses.sJUIHeader:"F"==o&&(o=a.oClasses.sJUIFooter);-1!=o.indexOf(".")?(w=o.split("."),e.id=w[0].substr(1,w[0].length-1),e.className=w[1]):"#"==o.charAt(0)?e.id=o.substr(1,o.length-1):e.className=o;m+=k}c.appendChild(e);c=e}else if(">"==g)c=c.parentNode;else if("l"==g&&a.oFeatures.bPaginate&&a.oFeatures.bLengthChange)i=ya(a),f=1;else if("f"==g&&a.oFeatures.bFilter)i=za(a),f=1;else if("r"==g&&a.oFeatures.bProcessing)i=Aa(a),f=
+1;else if("t"==g)i=Ba(a),f=1;else if("i"==g&&a.oFeatures.bInfo)i=Ca(a),f=1;else if("p"==g&&a.oFeatures.bPaginate)i=Da(a),f=1;else if(0!==j.ext.aoFeatures.length){e=j.ext.aoFeatures;k=0;for(w=e.length;k<w;k++)if(g==e[k].cFeature){(i=e[k].fnInit(a))&&(f=1);break}}1==f&&null!==i&&("object"!==typeof a.aanFeatures[g]&&(a.aanFeatures[g]=[]),a.aanFeatures[g].push(i),c.appendChild(i))}b.parentNode.replaceChild(a.nTableWrapper,b)}function V(a,b){var c=h(b).children("tr"),d,i,f,g,e,j,o,k,m,p;a.splice(0,a.length);
+f=0;for(j=c.length;f<j;f++)a.push([]);f=0;for(j=c.length;f<j;f++){d=c[f];for(i=d.firstChild;i;){if("TD"==i.nodeName.toUpperCase()||"TH"==i.nodeName.toUpperCase()){k=1*i.getAttribute("colspan");m=1*i.getAttribute("rowspan");k=!k||0===k||1===k?1:k;m=!m||0===m||1===m?1:m;g=0;for(e=a[f];e[g];)g++;o=g;p=1===k?!0:!1;for(e=0;e<k;e++)for(g=0;g<m;g++)a[f+g][o+e]={cell:i,unique:p},a[f+g].nTr=d}i=i.nextSibling}}}function N(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],V(c,b)));for(var b=0,i=c.length;b<i;b++)for(var f=
+0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function wa(a){if(a.bAjaxDataGet){a.iDraw++;E(a,!0);var b=Ea(a);ka(a,b);a.fnServerData.call(a.oInstance,a.sAjaxSource,b,function(b){Fa(a,b)},a);return!1}return!0}function Ea(a){var b=a.aoColumns.length,c=[],d,i,f,g;c.push({name:"sEcho",value:a.iDraw});c.push({name:"iColumns",value:b});c.push({name:"sColumns",value:M(a)});c.push({name:"iDisplayStart",value:a._iDisplayStart});c.push({name:"iDisplayLength",
+value:!1!==a.oFeatures.bPaginate?a._iDisplayLength:-1});for(f=0;f<b;f++)d=a.aoColumns[f].mData,c.push({name:"mDataProp_"+f,value:"function"===typeof d?"function":d});if(!1!==a.oFeatures.bFilter){c.push({name:"sSearch",value:a.oPreviousSearch.sSearch});c.push({name:"bRegex",value:a.oPreviousSearch.bRegex});for(f=0;f<b;f++)c.push({name:"sSearch_"+f,value:a.aoPreSearchCols[f].sSearch}),c.push({name:"bRegex_"+f,value:a.aoPreSearchCols[f].bRegex}),c.push({name:"bSearchable_"+f,value:a.aoColumns[f].bSearchable})}if(!1!==
+a.oFeatures.bSort){var e=0;d=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(f=0;f<d.length;f++){i=a.aoColumns[d[f][0]].aDataSort;for(g=0;g<i.length;g++)c.push({name:"iSortCol_"+e,value:i[g]}),c.push({name:"sSortDir_"+e,value:d[f][1]}),e++}c.push({name:"iSortingCols",value:e});for(f=0;f<b;f++)c.push({name:"bSortable_"+f,value:a.aoColumns[f].bSortable})}return c}function ka(a,b){A(a,"aoServerParams","serverParams",[b])}function Fa(a,b){if(b.sEcho!==n){if(1*b.sEcho<
+a.iDraw)return;a.iDraw=1*b.sEcho}(!a.oScroll.bInfinite||a.oScroll.bInfinite&&(a.bSorted||a.bFiltered))&&ga(a);a._iRecordsTotal=parseInt(b.iTotalRecords,10);a._iRecordsDisplay=parseInt(b.iTotalDisplayRecords,10);var c=M(a),c=b.sColumns!==n&&""!==c&&b.sColumns!=c,d;c&&(d=u(a,b.sColumns));for(var i=Q(a.sAjaxDataProp)(b),f=0,g=i.length;f<g;f++)if(c){for(var e=[],h=0,j=a.aoColumns.length;h<j;h++)e.push(i[f][d[h]]);H(a,e)}else H(a,i[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;x(a);a.bAjaxDataGet=
+!0;E(a,!1)}function za(a){var b=a.oPreviousSearch,c=a.oLanguage.sSearch,c=-1!==c.indexOf("_INPUT_")?c.replace("_INPUT_",'<input type="text" />'):""===c?'<input type="text" />':c+' <input type="text" />',d=l.createElement("div");d.className=a.oClasses.sFilter;d.innerHTML="<label>"+c+"</label>";a.aanFeatures.f||(d.id=a.sTableId+"_filter");c=h('input[type="text"]',d);d._DT_Input=c[0];c.val(b.sSearch.replace('"',"&quot;"));c.bind("keyup.DT",function(){for(var c=a.aanFeatures.f,d=this.value===""?"":this.value,
+g=0,e=c.length;g<e;g++)c[g]!=h(this).parents("div.dataTables_filter")[0]&&h(c[g]._DT_Input).val(d);d!=b.sSearch&&K(a,{sSearch:d,bRegex:b.bRegex,bSmart:b.bSmart,bCaseInsensitive:b.bCaseInsensitive})});c.attr("aria-controls",a.sTableId).bind("keypress.DT",function(a){if(a.keyCode==13)return false});return d}function K(a,b,c){var d=a.oPreviousSearch,i=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};if(a.oFeatures.bServerSide)f(b);
+else{Ga(a,b.sSearch,c,b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<a.aoPreSearchCols.length;b++)Ha(a,i[b].sSearch,b,i[b].bRegex,i[b].bSmart,i[b].bCaseInsensitive);Ia(a)}a.bFiltered=!0;h(a.oInstance).trigger("filter",a);a._iDisplayStart=0;y(a);x(a);la(a,0)}function Ia(a){for(var b=j.ext.afnFiltering,c=r(a,"bSearchable"),d=0,i=b.length;d<i;d++)for(var f=0,g=0,e=a.aiDisplay.length;g<e;g++){var h=a.aiDisplay[g-f];b[d](a,Y(a,h,"filter",c),h)||(a.aiDisplay.splice(g-f,1),f++)}}function Ha(a,b,c,
+d,i,f){if(""!==b)for(var g=0,b=ma(b,d,i,f),d=a.aiDisplay.length-1;0<=d;d--)i=Ja(v(a,a.aiDisplay[d],c,"filter"),a.aoColumns[c].sType),b.test(i)||(a.aiDisplay.splice(d,1),g++)}function Ga(a,b,c,d,i,f){d=ma(b,d,i,f);i=a.oPreviousSearch;c||(c=0);0!==j.ext.afnFiltering.length&&(c=1);if(0>=b.length)a.aiDisplay.splice(0,a.aiDisplay.length),a.aiDisplay=a.aiDisplayMaster.slice();else if(a.aiDisplay.length==a.aiDisplayMaster.length||i.sSearch.length>b.length||1==c||0!==b.indexOf(i.sSearch)){a.aiDisplay.splice(0,
+a.aiDisplay.length);la(a,1);for(b=0;b<a.aiDisplayMaster.length;b++)d.test(a.asDataSearch[b])&&a.aiDisplay.push(a.aiDisplayMaster[b])}else for(b=c=0;b<a.asDataSearch.length;b++)d.test(a.asDataSearch[b])||(a.aiDisplay.splice(b-c,1),c++)}function la(a,b){if(!a.oFeatures.bServerSide){a.asDataSearch=[];for(var c=r(a,"bSearchable"),d=1===b?a.aiDisplayMaster:a.aiDisplay,i=0,f=d.length;i<f;i++)a.asDataSearch[i]=na(a,Y(a,d[i],"filter",c))}}function na(a,b){var c=b.join("  ");-1!==c.indexOf("&")&&(c=h("<div>").html(c).text());
+return c.replace(/[\n\r]/g," ")}function ma(a,b,c,d){if(c)return a=b?a.split(" "):oa(a).split(" "),a="^(?=.*?"+a.join(")(?=.*?")+").*$",RegExp(a,d?"i":"");a=b?a:oa(a);return RegExp(a,d?"i":"")}function Ja(a,b){return"function"===typeof j.ext.ofnSearch[b]?j.ext.ofnSearch[b](a):null===a?"":"html"==b?a.replace(/[\r\n]/g," ").replace(/<.*?>/g,""):"string"===typeof a?a.replace(/[\r\n]/g," "):a}function oa(a){return a.replace(RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),
+"\\$1")}function Ca(a){var b=l.createElement("div");b.className=a.oClasses.sInfo;a.aanFeatures.i||(a.aoDrawCallback.push({fn:Ka,sName:"information"}),b.id=a.sTableId+"_info");a.nTable.setAttribute("aria-describedby",a.sTableId+"_info");return b}function Ka(a){if(a.oFeatures.bInfo&&0!==a.aanFeatures.i.length){var b=a.oLanguage,c=a._iDisplayStart+1,d=a.fnDisplayEnd(),i=a.fnRecordsTotal(),f=a.fnRecordsDisplay(),g;g=0===f?b.sInfoEmpty:b.sInfo;f!=i&&(g+=" "+b.sInfoFiltered);g+=b.sInfoPostFix;g=ja(a,g);
+null!==b.fnInfoCallback&&(g=b.fnInfoCallback.call(a.oInstance,a,c,d,i,f,g));a=a.aanFeatures.i;b=0;for(c=a.length;b<c;b++)h(a[b]).html(g)}}function ja(a,b){var c=a.fnFormatNumber(a._iDisplayStart+1),d=a.fnDisplayEnd(),d=a.fnFormatNumber(d),i=a.fnRecordsDisplay(),i=a.fnFormatNumber(i),f=a.fnRecordsTotal(),f=a.fnFormatNumber(f);a.oScroll.bInfinite&&(c=a.fnFormatNumber(1));return b.replace(/_START_/g,c).replace(/_END_/g,d).replace(/_TOTAL_/g,i).replace(/_MAX_/g,f)}function ba(a){var b,c,d=a.iInitDisplayStart;
+if(!1===a.bInitialised)setTimeout(function(){ba(a)},200);else{xa(a);va(a);W(a,a.aoHeader);a.nTFoot&&W(a,a.aoFooter);E(a,!0);a.oFeatures.bAutoWidth&&da(a);b=0;for(c=a.aoColumns.length;b<c;b++)null!==a.aoColumns[b].sWidth&&(a.aoColumns[b].nTh.style.width=q(a.aoColumns[b].sWidth));a.oFeatures.bSort?O(a):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(a.aiDisplay=a.aiDisplayMaster.slice(),y(a),x(a));null!==a.sAjaxSource&&!a.oFeatures.bServerSide?(c=[],ka(a,c),a.fnServerData.call(a.oInstance,a.sAjaxSource,
+c,function(c){var f=a.sAjaxDataProp!==""?Q(a.sAjaxDataProp)(c):c;for(b=0;b<f.length;b++)H(a,f[b]);a.iInitDisplayStart=d;if(a.oFeatures.bSort)O(a);else{a.aiDisplay=a.aiDisplayMaster.slice();y(a);x(a)}E(a,false);$(a,c)},a)):a.oFeatures.bServerSide||(E(a,!1),$(a))}}function $(a,b){a._bInitComplete=!0;A(a,"aoInitComplete","init",[a,b])}function pa(a){var b=j.defaults.oLanguage;!a.sEmptyTable&&(a.sZeroRecords&&"No data available in table"===b.sEmptyTable)&&p(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&
+(a.sZeroRecords&&"Loading..."===b.sLoadingRecords)&&p(a,a,"sZeroRecords","sLoadingRecords")}function ya(a){if(a.oScroll.bInfinite)return null;var b='<select size="1" '+('name="'+a.sTableId+'_length"')+">",c,d,i=a.aLengthMenu;if(2==i.length&&"object"===typeof i[0]&&"object"===typeof i[1]){c=0;for(d=i[0].length;c<d;c++)b+='<option value="'+i[0][c]+'">'+i[1][c]+"</option>"}else{c=0;for(d=i.length;c<d;c++)b+='<option value="'+i[c]+'">'+i[c]+"</option>"}b+="</select>";i=l.createElement("div");a.aanFeatures.l||
+(i.id=a.sTableId+"_length");i.className=a.oClasses.sLength;i.innerHTML="<label>"+a.oLanguage.sLengthMenu.replace("_MENU_",b)+"</label>";h('select option[value="'+a._iDisplayLength+'"]',i).attr("selected",!0);h("select",i).bind("change.DT",function(){var b=h(this).val(),i=a.aanFeatures.l;c=0;for(d=i.length;c<d;c++)i[c]!=this.parentNode&&h("select",i[c]).val(b);a._iDisplayLength=parseInt(b,10);y(a);if(a.fnDisplayEnd()==a.fnRecordsDisplay()){a._iDisplayStart=a.fnDisplayEnd()-a._iDisplayLength;if(a._iDisplayStart<
+0)a._iDisplayStart=0}if(a._iDisplayLength==-1)a._iDisplayStart=0;x(a)});h("select",i).attr("aria-controls",a.sTableId);return i}function y(a){a._iDisplayEnd=!1===a.oFeatures.bPaginate?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength>a.aiDisplay.length||-1==a._iDisplayLength?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength}function Da(a){if(a.oScroll.bInfinite)return null;var b=l.createElement("div");b.className=a.oClasses.sPaging+a.sPaginationType;j.ext.oPagination[a.sPaginationType].fnInit(a,
+b,function(a){y(a);x(a)});a.aanFeatures.p||a.aoDrawCallback.push({fn:function(a){j.ext.oPagination[a.sPaginationType].fnUpdate(a,function(a){y(a);x(a)})},sName:"pagination"});return b}function qa(a,b){var c=a._iDisplayStart;if("number"===typeof b)a._iDisplayStart=b*a._iDisplayLength,a._iDisplayStart>a.fnRecordsDisplay()&&(a._iDisplayStart=0);else if("first"==b)a._iDisplayStart=0;else if("previous"==b)a._iDisplayStart=0<=a._iDisplayLength?a._iDisplayStart-a._iDisplayLength:0,0>a._iDisplayStart&&(a._iDisplayStart=
+0);else if("next"==b)0<=a._iDisplayLength?a._iDisplayStart+a._iDisplayLength<a.fnRecordsDisplay()&&(a._iDisplayStart+=a._iDisplayLength):a._iDisplayStart=0;else if("last"==b)if(0<=a._iDisplayLength){var d=parseInt((a.fnRecordsDisplay()-1)/a._iDisplayLength,10)+1;a._iDisplayStart=(d-1)*a._iDisplayLength}else a._iDisplayStart=0;else D(a,0,"Unknown paging action: "+b);h(a.oInstance).trigger("page",a);return c!=a._iDisplayStart}function Aa(a){var b=l.createElement("div");a.aanFeatures.r||(b.id=a.sTableId+
+"_processing");b.innerHTML=a.oLanguage.sProcessing;b.className=a.oClasses.sProcessing;a.nTable.parentNode.insertBefore(b,a.nTable);return b}function E(a,b){if(a.oFeatures.bProcessing)for(var c=a.aanFeatures.r,d=0,i=c.length;d<i;d++)c[d].style.visibility=b?"visible":"hidden";h(a.oInstance).trigger("processing",[a,b])}function Ba(a){if(""===a.oScroll.sX&&""===a.oScroll.sY)return a.nTable;var b=l.createElement("div"),c=l.createElement("div"),d=l.createElement("div"),i=l.createElement("div"),f=l.createElement("div"),
+g=l.createElement("div"),e=a.nTable.cloneNode(!1),j=a.nTable.cloneNode(!1),o=a.nTable.getElementsByTagName("thead")[0],k=0===a.nTable.getElementsByTagName("tfoot").length?null:a.nTable.getElementsByTagName("tfoot")[0],m=a.oClasses;c.appendChild(d);f.appendChild(g);i.appendChild(a.nTable);b.appendChild(c);b.appendChild(i);d.appendChild(e);e.appendChild(o);null!==k&&(b.appendChild(f),g.appendChild(j),j.appendChild(k));b.className=m.sScrollWrapper;c.className=m.sScrollHead;d.className=m.sScrollHeadInner;
+i.className=m.sScrollBody;f.className=m.sScrollFoot;g.className=m.sScrollFootInner;a.oScroll.bAutoCss&&(c.style.overflow="hidden",c.style.position="relative",f.style.overflow="hidden",i.style.overflow="auto");c.style.border="0";c.style.width="100%";f.style.border="0";d.style.width=""!==a.oScroll.sXInner?a.oScroll.sXInner:"100%";e.removeAttribute("id");e.style.marginLeft="0";a.nTable.style.marginLeft="0";null!==k&&(j.removeAttribute("id"),j.style.marginLeft="0");d=h(a.nTable).children("caption");0<
+d.length&&(d=d[0],"top"===d._captionSide?e.appendChild(d):"bottom"===d._captionSide&&k&&j.appendChild(d));""!==a.oScroll.sX&&(c.style.width=q(a.oScroll.sX),i.style.width=q(a.oScroll.sX),null!==k&&(f.style.width=q(a.oScroll.sX)),h(i).scroll(function(){c.scrollLeft=this.scrollLeft;if(k!==null)f.scrollLeft=this.scrollLeft}));""!==a.oScroll.sY&&(i.style.height=q(a.oScroll.sY));a.aoDrawCallback.push({fn:La,sName:"scrolling"});a.oScroll.bInfinite&&h(i).scroll(function(){if(!a.bDrawing&&h(this).scrollTop()!==
+0&&h(this).scrollTop()+h(this).height()>h(a.nTable).height()-a.oScroll.iLoadGap&&a.fnDisplayEnd()<a.fnRecordsDisplay()){qa(a,"next");y(a);x(a)}});a.nScrollHead=c;a.nScrollFoot=f;return b}function La(a){var b=a.nScrollHead.getElementsByTagName("div")[0],c=b.getElementsByTagName("table")[0],d=a.nTable.parentNode,i,f,g,e,j,o,k,m,p=[],n=[],l=null!==a.nTFoot?a.nScrollFoot.getElementsByTagName("div")[0]:null,R=null!==a.nTFoot?l.getElementsByTagName("table")[0]:null,r=a.oBrowser.bScrollOversize,s=function(a){k=
+a.style;k.paddingTop="0";k.paddingBottom="0";k.borderTopWidth="0";k.borderBottomWidth="0";k.height=0};h(a.nTable).children("thead, tfoot").remove();i=h(a.nTHead).clone()[0];a.nTable.insertBefore(i,a.nTable.childNodes[0]);g=a.nTHead.getElementsByTagName("tr");e=i.getElementsByTagName("tr");null!==a.nTFoot&&(j=h(a.nTFoot).clone()[0],a.nTable.insertBefore(j,a.nTable.childNodes[1]),o=a.nTFoot.getElementsByTagName("tr"),j=j.getElementsByTagName("tr"));""===a.oScroll.sX&&(d.style.width="100%",b.parentNode.style.width=
+"100%");var t=N(a,i);i=0;for(f=t.length;i<f;i++)m=G(a,i),t[i].style.width=a.aoColumns[m].sWidth;null!==a.nTFoot&&C(function(a){a.style.width=""},j);a.oScroll.bCollapse&&""!==a.oScroll.sY&&(d.style.height=d.offsetHeight+a.nTHead.offsetHeight+"px");i=h(a.nTable).outerWidth();if(""===a.oScroll.sX){if(a.nTable.style.width="100%",r&&(h("tbody",d).height()>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(h(a.nTable).outerWidth()-a.oScroll.iBarWidth)}else""!==a.oScroll.sXInner?a.nTable.style.width=
+q(a.oScroll.sXInner):i==h(d).width()&&h(d).height()<h(a.nTable).height()?(a.nTable.style.width=q(i-a.oScroll.iBarWidth),h(a.nTable).outerWidth()>i-a.oScroll.iBarWidth&&(a.nTable.style.width=q(i))):a.nTable.style.width=q(i);i=h(a.nTable).outerWidth();C(s,e);C(function(a){p.push(q(h(a).width()))},e);C(function(a,b){a.style.width=p[b]},g);h(e).height(0);null!==a.nTFoot&&(C(s,j),C(function(a){n.push(q(h(a).width()))},j),C(function(a,b){a.style.width=n[b]},o),h(j).height(0));C(function(a,b){a.innerHTML=
+"";a.style.width=p[b]},e);null!==a.nTFoot&&C(function(a,b){a.innerHTML="";a.style.width=n[b]},j);if(h(a.nTable).outerWidth()<i){g=d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")?i+a.oScroll.iBarWidth:i;if(r&&(d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(g-a.oScroll.iBarWidth);d.style.width=q(g);a.nScrollHead.style.width=q(g);null!==a.nTFoot&&(a.nScrollFoot.style.width=q(g));""===a.oScroll.sX?D(a,1,"The table cannot fit into the current element which will cause column misalignment. The table has been drawn at its minimum possible width."):
+""!==a.oScroll.sXInner&&D(a,1,"The table cannot fit into the current element which will cause column misalignment. Increase the sScrollXInner value or remove it to allow automatic calculation")}else d.style.width=q("100%"),a.nScrollHead.style.width=q("100%"),null!==a.nTFoot&&(a.nScrollFoot.style.width=q("100%"));""===a.oScroll.sY&&r&&(d.style.height=q(a.nTable.offsetHeight+a.oScroll.iBarWidth));""!==a.oScroll.sY&&a.oScroll.bCollapse&&(d.style.height=q(a.oScroll.sY),r=""!==a.oScroll.sX&&a.nTable.offsetWidth>
+d.offsetWidth?a.oScroll.iBarWidth:0,a.nTable.offsetHeight<d.offsetHeight&&(d.style.height=q(a.nTable.offsetHeight+r)));r=h(a.nTable).outerWidth();c.style.width=q(r);b.style.width=q(r);c=h(a.nTable).height()>d.clientHeight||"scroll"==h(d).css("overflow-y");b.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px";null!==a.nTFoot&&(R.style.width=q(r),l.style.width=q(r),l.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px");h(d).scroll();if(a.bSorted||a.bFiltered)d.scrollTop=0}function C(a,b,c){for(var d=
+0,i=0,f=b.length,g,e;i<f;){g=b[i].firstChild;for(e=c?c[i].firstChild:null;g;)1===g.nodeType&&(c?a(g,e,d):a(g,d),d++),g=g.nextSibling,e=c?e.nextSibling:null;i++}}function Ma(a,b){if(!a||null===a||""===a)return 0;b||(b=l.body);var c,d=l.createElement("div");d.style.width=q(a);b.appendChild(d);c=d.offsetWidth;b.removeChild(d);return c}function da(a){var b=0,c,d=0,i=a.aoColumns.length,f,e,j=h("th",a.nTHead),o=a.nTable.getAttribute("width");e=a.nTable.parentNode;for(f=0;f<i;f++)a.aoColumns[f].bVisible&&
+(d++,null!==a.aoColumns[f].sWidth&&(c=Ma(a.aoColumns[f].sWidthOrig,e),null!==c&&(a.aoColumns[f].sWidth=q(c)),b++));if(i==j.length&&0===b&&d==i&&""===a.oScroll.sX&&""===a.oScroll.sY)for(f=0;f<a.aoColumns.length;f++)c=h(j[f]).width(),null!==c&&(a.aoColumns[f].sWidth=q(c));else{b=a.nTable.cloneNode(!1);f=a.nTHead.cloneNode(!0);d=l.createElement("tbody");c=l.createElement("tr");b.removeAttribute("id");b.appendChild(f);null!==a.nTFoot&&(b.appendChild(a.nTFoot.cloneNode(!0)),C(function(a){a.style.width=
+""},b.getElementsByTagName("tr")));b.appendChild(d);d.appendChild(c);d=h("thead th",b);0===d.length&&(d=h("tbody tr:eq(0)>td",b));j=N(a,f);for(f=d=0;f<i;f++){var k=a.aoColumns[f];k.bVisible&&null!==k.sWidthOrig&&""!==k.sWidthOrig?j[f-d].style.width=q(k.sWidthOrig):k.bVisible?j[f-d].style.width="":d++}for(f=0;f<i;f++)a.aoColumns[f].bVisible&&(d=Na(a,f),null!==d&&(d=d.cloneNode(!0),""!==a.aoColumns[f].sContentPadding&&(d.innerHTML+=a.aoColumns[f].sContentPadding),c.appendChild(d)));e.appendChild(b);
+""!==a.oScroll.sX&&""!==a.oScroll.sXInner?b.style.width=q(a.oScroll.sXInner):""!==a.oScroll.sX?(b.style.width="",h(b).width()<e.offsetWidth&&(b.style.width=q(e.offsetWidth))):""!==a.oScroll.sY?b.style.width=q(e.offsetWidth):o&&(b.style.width=q(o));b.style.visibility="hidden";Oa(a,b);i=h("tbody tr:eq(0)",b).children();0===i.length&&(i=N(a,h("thead",b)[0]));if(""!==a.oScroll.sX){for(f=d=e=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=null===a.aoColumns[f].sWidthOrig?e+h(i[d]).outerWidth():
+e+(parseInt(a.aoColumns[f].sWidth.replace("px",""),10)+(h(i[d]).outerWidth()-h(i[d]).width())),d++);b.style.width=q(e);a.nTable.style.width=q(e)}for(f=d=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=h(i[d]).width(),null!==e&&0<e&&(a.aoColumns[f].sWidth=q(e)),d++);i=h(b).css("width");a.nTable.style.width=-1!==i.indexOf("%")?i:q(h(b).outerWidth());b.parentNode.removeChild(b)}o&&(a.nTable.style.width=q(o))}function Oa(a,b){""===a.oScroll.sX&&""!==a.oScroll.sY?(h(b).width(),b.style.width=q(h(b).outerWidth()-
+a.oScroll.iBarWidth)):""!==a.oScroll.sX&&(b.style.width=q(h(b).outerWidth()))}function Na(a,b){var c=Pa(a,b);if(0>c)return null;if(null===a.aoData[c].nTr){var d=l.createElement("td");d.innerHTML=v(a,c,b,"");return d}return J(a,c)[b]}function Pa(a,b){for(var c=-1,d=-1,i=0;i<a.aoData.length;i++){var e=v(a,i,b,"display")+"",e=e.replace(/<.*?>/g,"");e.length>c&&(c=e.length,d=i)}return d}function q(a){if(null===a)return"0px";if("number"==typeof a)return 0>a?"0px":a+"px";var b=a.charCodeAt(a.length-1);
+return 48>b||57<b?a:a+"px"}function Qa(){var a=l.createElement("p"),b=a.style;b.width="100%";b.height="200px";b.padding="0px";var c=l.createElement("div"),b=c.style;b.position="absolute";b.top="0px";b.left="0px";b.visibility="hidden";b.width="200px";b.height="150px";b.padding="0px";b.overflow="hidden";c.appendChild(a);l.body.appendChild(c);b=a.offsetWidth;c.style.overflow="scroll";a=a.offsetWidth;b==a&&(a=c.clientWidth);l.body.removeChild(c);return b-a}function O(a,b){var c,d,i,e,g,k,o=[],m=[],p=
+j.ext.oSort,l=a.aoData,q=a.aoColumns,G=a.oLanguage.oAria;if(!a.oFeatures.bServerSide&&(0!==a.aaSorting.length||null!==a.aaSortingFixed)){o=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(c=0;c<o.length;c++)if(d=o[c][0],i=R(a,d),e=a.aoColumns[d].sSortDataType,j.ext.afnSortData[e])if(g=j.ext.afnSortData[e].call(a.oInstance,a,d,i),g.length===l.length){i=0;for(e=l.length;i<e;i++)F(a,i,d,g[i])}else D(a,0,"Returned data sort array (col "+d+") is the wrong length");c=
+0;for(d=a.aiDisplayMaster.length;c<d;c++)m[a.aiDisplayMaster[c]]=c;var r=o.length,s;c=0;for(d=l.length;c<d;c++)for(i=0;i<r;i++){s=q[o[i][0]].aDataSort;g=0;for(k=s.length;g<k;g++)e=q[s[g]].sType,e=p[(e?e:"string")+"-pre"],l[c]._aSortData[s[g]]=e?e(v(a,c,s[g],"sort")):v(a,c,s[g],"sort")}a.aiDisplayMaster.sort(function(a,b){var c,d,e,i,f;for(c=0;c<r;c++){f=q[o[c][0]].aDataSort;d=0;for(e=f.length;d<e;d++)if(i=q[f[d]].sType,i=p[(i?i:"string")+"-"+o[c][1]](l[a]._aSortData[f[d]],l[b]._aSortData[f[d]]),0!==
+i)return i}return p["numeric-asc"](m[a],m[b])})}(b===n||b)&&!a.oFeatures.bDeferRender&&P(a);c=0;for(d=a.aoColumns.length;c<d;c++)e=q[c].sTitle.replace(/<.*?>/g,""),i=q[c].nTh,i.removeAttribute("aria-sort"),i.removeAttribute("aria-label"),q[c].bSortable?0<o.length&&o[0][0]==c?(i.setAttribute("aria-sort","asc"==o[0][1]?"ascending":"descending"),i.setAttribute("aria-label",e+("asc"==(q[c].asSorting[o[0][2]+1]?q[c].asSorting[o[0][2]+1]:q[c].asSorting[0])?G.sSortAscending:G.sSortDescending))):i.setAttribute("aria-label",
+e+("asc"==q[c].asSorting[0]?G.sSortAscending:G.sSortDescending)):i.setAttribute("aria-label",e);a.bSorted=!0;h(a.oInstance).trigger("sort",a);a.oFeatures.bFilter?K(a,a.oPreviousSearch,1):(a.aiDisplay=a.aiDisplayMaster.slice(),a._iDisplayStart=0,y(a),x(a))}function ia(a,b,c,d){Ra(b,{},function(b){if(!1!==a.aoColumns[c].bSortable){var e=function(){var d,e;if(b.shiftKey){for(var f=!1,h=0;h<a.aaSorting.length;h++)if(a.aaSorting[h][0]==c){f=!0;d=a.aaSorting[h][0];e=a.aaSorting[h][2]+1;a.aoColumns[d].asSorting[e]?
+(a.aaSorting[h][1]=a.aoColumns[d].asSorting[e],a.aaSorting[h][2]=e):a.aaSorting.splice(h,1);break}!1===f&&a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0])}else 1==a.aaSorting.length&&a.aaSorting[0][0]==c?(d=a.aaSorting[0][0],e=a.aaSorting[0][2]+1,a.aoColumns[d].asSorting[e]||(e=0),a.aaSorting[0][1]=a.aoColumns[d].asSorting[e],a.aaSorting[0][2]=e):(a.aaSorting.splice(0,a.aaSorting.length),a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0]));O(a)};a.oFeatures.bProcessing?(E(a,!0),setTimeout(function(){e();
+a.oFeatures.bServerSide||E(a,!1)},0)):e();"function"==typeof d&&d(a)}})}function P(a){var b,c,d,e,f,g=a.aoColumns.length,j=a.oClasses;for(b=0;b<g;b++)a.aoColumns[b].bSortable&&h(a.aoColumns[b].nTh).removeClass(j.sSortAsc+" "+j.sSortDesc+" "+a.aoColumns[b].sSortingClass);c=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(b=0;b<a.aoColumns.length;b++)if(a.aoColumns[b].bSortable){f=a.aoColumns[b].sSortingClass;e=-1;for(d=0;d<c.length;d++)if(c[d][0]==b){f="asc"==c[d][1]?
+j.sSortAsc:j.sSortDesc;e=d;break}h(a.aoColumns[b].nTh).addClass(f);a.bJUI&&(f=h("span."+j.sSortIcon,a.aoColumns[b].nTh),f.removeClass(j.sSortJUIAsc+" "+j.sSortJUIDesc+" "+j.sSortJUI+" "+j.sSortJUIAscAllowed+" "+j.sSortJUIDescAllowed),f.addClass(-1==e?a.aoColumns[b].sSortingClassJUI:"asc"==c[e][1]?j.sSortJUIAsc:j.sSortJUIDesc))}else h(a.aoColumns[b].nTh).addClass(a.aoColumns[b].sSortingClass);f=j.sSortColumn;if(a.oFeatures.bSort&&a.oFeatures.bSortClasses){a=J(a);e=[];for(b=0;b<g;b++)e.push("");b=0;
+for(d=1;b<c.length;b++)j=parseInt(c[b][0],10),e[j]=f+d,3>d&&d++;f=RegExp(f+"[123]");var o;b=0;for(c=a.length;b<c;b++)j=b%g,d=a[b].className,o=e[j],j=d.replace(f,o),j!=d?a[b].className=h.trim(j):0<o.length&&-1==d.indexOf(o)&&(a[b].className=d+" "+o)}}function ra(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b,c;b=a.oScroll.bInfinite;var d={iCreate:(new Date).getTime(),iStart:b?0:a._iDisplayStart,iEnd:b?a._iDisplayLength:a._iDisplayEnd,iLength:a._iDisplayLength,aaSorting:h.extend(!0,[],a.aaSorting),
+oSearch:h.extend(!0,{},a.oPreviousSearch),aoSearchCols:h.extend(!0,[],a.aoPreSearchCols),abVisCols:[]};b=0;for(c=a.aoColumns.length;b<c;b++)d.abVisCols.push(a.aoColumns[b].bVisible);A(a,"aoStateSaveParams","stateSaveParams",[a,d]);a.fnStateSave.call(a.oInstance,a,d)}}function Sa(a,b){if(a.oFeatures.bStateSave){var c=a.fnStateLoad.call(a.oInstance,a);if(c){var d=A(a,"aoStateLoadParams","stateLoadParams",[a,c]);if(-1===h.inArray(!1,d)){a.oLoadedState=h.extend(!0,{},c);a._iDisplayStart=c.iStart;a.iInitDisplayStart=
+c.iStart;a._iDisplayEnd=c.iEnd;a._iDisplayLength=c.iLength;a.aaSorting=c.aaSorting.slice();a.saved_aaSorting=c.aaSorting.slice();h.extend(a.oPreviousSearch,c.oSearch);h.extend(!0,a.aoPreSearchCols,c.aoSearchCols);b.saved_aoColumns=[];for(d=0;d<c.abVisCols.length;d++)b.saved_aoColumns[d]={},b.saved_aoColumns[d].bVisible=c.abVisCols[d];A(a,"aoStateLoaded","stateLoaded",[a,c])}}}}function s(a){for(var b=0;b<j.settings.length;b++)if(j.settings[b].nTable===a)return j.settings[b];return null}function T(a){for(var b=
+[],a=a.aoData,c=0,d=a.length;c<d;c++)null!==a[c].nTr&&b.push(a[c].nTr);return b}function J(a,b){var c=[],d,e,f,g,h,j;e=0;var o=a.aoData.length;b!==n&&(e=b,o=b+1);for(f=e;f<o;f++)if(j=a.aoData[f],null!==j.nTr){e=[];for(d=j.nTr.firstChild;d;)g=d.nodeName.toLowerCase(),("td"==g||"th"==g)&&e.push(d),d=d.nextSibling;g=d=0;for(h=a.aoColumns.length;g<h;g++)a.aoColumns[g].bVisible?c.push(e[g-d]):(c.push(j._anHidden[g]),d++)}return c}function D(a,b,c){a=null===a?"DataTables warning: "+c:"DataTables warning (table id = '"+
+a.sTableId+"'): "+c;if(0===b)if("alert"==j.ext.sErrMode)alert(a);else throw Error(a);else X.console&&console.log&&console.log(a)}function p(a,b,c,d){d===n&&(d=c);b[c]!==n&&(a[d]=b[c])}function Ta(a,b){var c,d;for(d in b)b.hasOwnProperty(d)&&(c=b[d],"object"===typeof e[d]&&null!==c&&!1===h.isArray(c)?h.extend(!0,a[d],c):a[d]=c);return a}function Ra(a,b,c){h(a).bind("click.DT",b,function(b){a.blur();c(b)}).bind("keypress.DT",b,function(a){13===a.which&&c(a)}).bind("selectstart.DT",function(){return!1})}
+function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function A(a,b,c,d){for(var b=a[b],e=[],f=b.length-1;0<=f;f--)e.push(b[f].fn.apply(a.oInstance,d));null!==c&&h(a.oInstance).trigger(c,d);return e}function Ua(a){var b=h('<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden"><div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;"><div id="DT_BrowserTest" style="width:100%; height:10px;"></div></div></div>')[0];l.body.appendChild(b);a.oBrowser.bScrollOversize=
+100===h("#DT_BrowserTest",b)[0].offsetWidth?!0:!1;l.body.removeChild(b)}function Va(a){return function(){var b=[s(this[j.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return j.ext.oApi[a].apply(this,b)}}var U=/\[.*?\]$/,Wa=X.JSON?JSON.stringify:function(a){var b=typeof a;if("object"!==b||null===a)return"string"===b&&(a='"'+a+'"'),a+"";var c,d,e=[],f=h.isArray(a);for(c in a)d=a[c],b=typeof d,"string"===b?d='"'+d+'"':"object"===b&&null!==d&&(d=Wa(d)),e.push((f?"":'"'+c+'":')+d);return(f?
+"[":"{")+e+(f?"]":"}")};this.$=function(a,b){var c,d,e=[],f;d=s(this[j.ext.iApiIndex]);var g=d.aoData,o=d.aiDisplay,k=d.aiDisplayMaster;b||(b={});b=h.extend({},{filter:"none",order:"current",page:"all"},b);if("current"==b.page){c=d._iDisplayStart;for(d=d.fnDisplayEnd();c<d;c++)(f=g[o[c]].nTr)&&e.push(f)}else if("current"==b.order&&"none"==b.filter){c=0;for(d=k.length;c<d;c++)(f=g[k[c]].nTr)&&e.push(f)}else if("current"==b.order&&"applied"==b.filter){c=0;for(d=o.length;c<d;c++)(f=g[o[c]].nTr)&&e.push(f)}else if("original"==
+b.order&&"none"==b.filter){c=0;for(d=g.length;c<d;c++)(f=g[c].nTr)&&e.push(f)}else if("original"==b.order&&"applied"==b.filter){c=0;for(d=g.length;c<d;c++)f=g[c].nTr,-1!==h.inArray(c,o)&&f&&e.push(f)}else D(d,1,"Unknown selection options");e=h(e);c=e.filter(a);e=e.find(a);return h([].concat(h.makeArray(c),h.makeArray(e)))};this._=function(a,b){var c=[],d,e,f=this.$(a,b);d=0;for(e=f.length;d<e;d++)c.push(this.fnGetData(f[d]));return c};this.fnAddData=function(a,b){if(0===a.length)return[];var c=[],
+d,e=s(this[j.ext.iApiIndex]);if("object"===typeof a[0]&&null!==a[0])for(var f=0;f<a.length;f++){d=H(e,a[f]);if(-1==d)return c;c.push(d)}else{d=H(e,a);if(-1==d)return c;c.push(d)}e.aiDisplay=e.aiDisplayMaster.slice();(b===n||b)&&aa(e);return c};this.fnAdjustColumnSizing=function(a){var b=s(this[j.ext.iApiIndex]);k(b);a===n||a?this.fnDraw(!1):(""!==b.oScroll.sX||""!==b.oScroll.sY)&&this.oApi._fnScrollDraw(b)};this.fnClearTable=function(a){var b=s(this[j.ext.iApiIndex]);ga(b);(a===n||a)&&x(b)};this.fnClose=
+function(a){for(var b=s(this[j.ext.iApiIndex]),c=0;c<b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return(a=b.aoOpenRows[c].nTr.parentNode)&&a.removeChild(b.aoOpenRows[c].nTr),b.aoOpenRows.splice(c,1),0;return 1};this.fnDeleteRow=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,a="object"===typeof a?I(d,a):a,g=d.aoData.splice(a,1);e=0;for(f=d.aoData.length;e<f;e++)null!==d.aoData[e].nTr&&(d.aoData[e].nTr._DT_RowIndex=e);e=h.inArray(a,d.aiDisplay);d.asDataSearch.splice(e,1);ha(d.aiDisplayMaster,
+a);ha(d.aiDisplay,a);"function"===typeof b&&b.call(this,d,g);d._iDisplayStart>=d.fnRecordsDisplay()&&(d._iDisplayStart-=d._iDisplayLength,0>d._iDisplayStart&&(d._iDisplayStart=0));if(c===n||c)y(d),x(d);return g};this.fnDestroy=function(a){var b=s(this[j.ext.iApiIndex]),c=b.nTableWrapper.parentNode,d=b.nTBody,i,f,a=a===n?!1:a;b.bDestroying=!0;A(b,"aoDestroyCallback","destroy",[b]);if(!a){i=0;for(f=b.aoColumns.length;i<f;i++)!1===b.aoColumns[i].bVisible&&this.fnSetColumnVis(i,!0)}h(b.nTableWrapper).find("*").andSelf().unbind(".DT");
+h("tbody>tr>td."+b.oClasses.sRowEmpty,b.nTable).parent().remove();b.nTable!=b.nTHead.parentNode&&(h(b.nTable).children("thead").remove(),b.nTable.appendChild(b.nTHead));b.nTFoot&&b.nTable!=b.nTFoot.parentNode&&(h(b.nTable).children("tfoot").remove(),b.nTable.appendChild(b.nTFoot));b.nTable.parentNode.removeChild(b.nTable);h(b.nTableWrapper).remove();b.aaSorting=[];b.aaSortingFixed=[];P(b);h(T(b)).removeClass(b.asStripeClasses.join(" "));h("th, td",b.nTHead).removeClass([b.oClasses.sSortable,b.oClasses.sSortableAsc,
+b.oClasses.sSortableDesc,b.oClasses.sSortableNone].join(" "));b.bJUI&&(h("th span."+b.oClasses.sSortIcon+", td span."+b.oClasses.sSortIcon,b.nTHead).remove(),h("th, td",b.nTHead).each(function(){var a=h("div."+b.oClasses.sSortJUIWrapper,this),c=a.contents();h(this).append(c);a.remove()}));!a&&b.nTableReinsertBefore?c.insertBefore(b.nTable,b.nTableReinsertBefore):a||c.appendChild(b.nTable);i=0;for(f=b.aoData.length;i<f;i++)null!==b.aoData[i].nTr&&d.appendChild(b.aoData[i].nTr);!0===b.oFeatures.bAutoWidth&&
+(b.nTable.style.width=q(b.sDestroyWidth));if(f=b.asDestroyStripes.length){a=h(d).children("tr");for(i=0;i<f;i++)a.filter(":nth-child("+f+"n + "+i+")").addClass(b.asDestroyStripes[i])}i=0;for(f=j.settings.length;i<f;i++)j.settings[i]==b&&j.settings.splice(i,1);e=b=null};this.fnDraw=function(a){var b=s(this[j.ext.iApiIndex]);!1===a?(y(b),x(b)):aa(b)};this.fnFilter=function(a,b,c,d,e,f){var g=s(this[j.ext.iApiIndex]);if(g.oFeatures.bFilter){if(c===n||null===c)c=!1;if(d===n||null===d)d=!0;if(e===n||null===
+e)e=!0;if(f===n||null===f)f=!0;if(b===n||null===b){if(K(g,{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f},1),e&&g.aanFeatures.f){b=g.aanFeatures.f;c=0;for(d=b.length;c<d;c++)try{b[c]._DT_Input!=l.activeElement&&h(b[c]._DT_Input).val(a)}catch(o){h(b[c]._DT_Input).val(a)}}}else h.extend(g.aoPreSearchCols[b],{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f}),K(g,g.oPreviousSearch,1)}};this.fnGetData=function(a,b){var c=s(this[j.ext.iApiIndex]);if(a!==n){var d=a;if("object"===typeof a){var e=a.nodeName.toLowerCase();
+"tr"===e?d=I(c,a):"td"===e&&(d=I(c,a.parentNode),b=fa(c,d,a))}return b!==n?v(c,d,b,""):c.aoData[d]!==n?c.aoData[d]._aData:null}return Z(c)};this.fnGetNodes=function(a){var b=s(this[j.ext.iApiIndex]);return a!==n?b.aoData[a]!==n?b.aoData[a].nTr:null:T(b)};this.fnGetPosition=function(a){var b=s(this[j.ext.iApiIndex]),c=a.nodeName.toUpperCase();return"TR"==c?I(b,a):"TD"==c||"TH"==c?(c=I(b,a.parentNode),a=fa(b,c,a),[c,R(b,a),a]):null};this.fnIsOpen=function(a){for(var b=s(this[j.ext.iApiIndex]),c=0;c<
+b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return!0;return!1};this.fnOpen=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e=T(d);if(-1!==h.inArray(a,e)){this.fnClose(a);var e=l.createElement("tr"),f=l.createElement("td");e.appendChild(f);f.className=c;f.colSpan=t(d);"string"===typeof b?f.innerHTML=b:h(f).html(b);b=h("tr",d.nTBody);-1!=h.inArray(a,b)&&h(e).insertAfter(a);d.aoOpenRows.push({nTr:e,nParent:a});return e}};this.fnPageChange=function(a,b){var c=s(this[j.ext.iApiIndex]);qa(c,a);
+y(c);(b===n||b)&&x(c)};this.fnSetColumnVis=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,g=d.aoColumns,h=d.aoData,o,m;if(g[a].bVisible!=b){if(b){for(e=f=0;e<a;e++)g[e].bVisible&&f++;m=f>=t(d);if(!m)for(e=a;e<g.length;e++)if(g[e].bVisible){o=e;break}e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(m?h[e].nTr.appendChild(h[e]._anHidden[a]):h[e].nTr.insertBefore(h[e]._anHidden[a],J(d,e)[o]))}else{e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(o=J(d,e)[a],h[e]._anHidden[a]=o,o.parentNode.removeChild(o))}g[a].bVisible=
+b;W(d,d.aoHeader);d.nTFoot&&W(d,d.aoFooter);e=0;for(f=d.aoOpenRows.length;e<f;e++)d.aoOpenRows[e].nTr.colSpan=t(d);if(c===n||c)k(d),x(d);ra(d)}};this.fnSettings=function(){return s(this[j.ext.iApiIndex])};this.fnSort=function(a){var b=s(this[j.ext.iApiIndex]);b.aaSorting=a;O(b)};this.fnSortListener=function(a,b,c){ia(s(this[j.ext.iApiIndex]),a,b,c)};this.fnUpdate=function(a,b,c,d,e){var f=s(this[j.ext.iApiIndex]),b="object"===typeof b?I(f,b):b;if(h.isArray(a)&&c===n){f.aoData[b]._aData=a.slice();
+for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else if(h.isPlainObject(a)&&c===n){f.aoData[b]._aData=h.extend(!0,{},a);for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else{F(f,b,c,a);var a=v(f,b,c,"display"),g=f.aoColumns[c];null!==g.fnRender&&(a=S(f,b,c),g.bUseRendered&&F(f,b,c,a));null!==f.aoData[b].nTr&&(J(f,b)[c].innerHTML=a)}c=h.inArray(b,f.aiDisplay);f.asDataSearch[c]=na(f,Y(f,b,"filter",r(f,"bSearchable")));(e===n||e)&&k(f);(d===n||d)&&aa(f);return 0};
+this.fnVersionCheck=j.ext.fnVersionCheck;this.oApi={_fnExternApiFunc:Va,_fnInitialise:ba,_fnInitComplete:$,_fnLanguageCompat:pa,_fnAddColumn:o,_fnColumnOptions:m,_fnAddData:H,_fnCreateTr:ea,_fnGatherData:ua,_fnBuildHead:va,_fnDrawHead:W,_fnDraw:x,_fnReDraw:aa,_fnAjaxUpdate:wa,_fnAjaxParameters:Ea,_fnAjaxUpdateDraw:Fa,_fnServerParams:ka,_fnAddOptionsHtml:xa,_fnFeatureHtmlTable:Ba,_fnScrollDraw:La,_fnAdjustColumnSizing:k,_fnFeatureHtmlFilter:za,_fnFilterComplete:K,_fnFilterCustom:Ia,_fnFilterColumn:Ha,
+_fnFilter:Ga,_fnBuildSearchArray:la,_fnBuildSearchRow:na,_fnFilterCreateSearch:ma,_fnDataToSearch:Ja,_fnSort:O,_fnSortAttachListener:ia,_fnSortingClasses:P,_fnFeatureHtmlPaginate:Da,_fnPageChange:qa,_fnFeatureHtmlInfo:Ca,_fnUpdateInfo:Ka,_fnFeatureHtmlLength:ya,_fnFeatureHtmlProcessing:Aa,_fnProcessingDisplay:E,_fnVisibleToColumnIndex:G,_fnColumnIndexToVisible:R,_fnNodeToDataIndex:I,_fnVisbleColumns:t,_fnCalculateEnd:y,_fnConvertToWidth:Ma,_fnCalculateColumnWidths:da,_fnScrollingWidthAdjust:Oa,_fnGetWidestNode:Na,
+_fnGetMaxLenString:Pa,_fnStringToCss:q,_fnDetectType:B,_fnSettingsFromNode:s,_fnGetDataMaster:Z,_fnGetTrNodes:T,_fnGetTdNodes:J,_fnEscapeRegex:oa,_fnDeleteIndex:ha,_fnReOrderIndex:u,_fnColumnOrdering:M,_fnLog:D,_fnClearTable:ga,_fnSaveState:ra,_fnLoadState:Sa,_fnCreateCookie:function(a,b,c,d,e){var f=new Date;f.setTime(f.getTime()+1E3*c);var c=X.location.pathname.split("/"),a=a+"_"+c.pop().replace(/[\/:]/g,"").toLowerCase(),g;null!==e?(g="function"===typeof h.parseJSON?h.parseJSON(b):eval("("+b+")"),
+b=e(a,g,f.toGMTString(),c.join("/")+"/")):b=a+"="+encodeURIComponent(b)+"; expires="+f.toGMTString()+"; path="+c.join("/")+"/";a=l.cookie.split(";");e=b.split(";")[0].length;f=[];if(4096<e+l.cookie.length+10){for(var j=0,o=a.length;j<o;j++)if(-1!=a[j].indexOf(d)){var k=a[j].split("=");try{(g=eval("("+decodeURIComponent(k[1])+")"))&&g.iCreate&&f.push({name:k[0],time:g.iCreate})}catch(m){}}for(f.sort(function(a,b){return b.time-a.time});4096<e+l.cookie.length+10;){if(0===f.length)return;d=f.pop();l.cookie=
+d.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+c.join("/")+"/"}}l.cookie=b},_fnReadCookie:function(a){for(var b=X.location.pathname.split("/"),a=a+"_"+b[b.length-1].replace(/[\/:]/g,"").toLowerCase()+"=",b=l.cookie.split(";"),c=0;c<b.length;c++){for(var d=b[c];" "==d.charAt(0);)d=d.substring(1,d.length);if(0===d.indexOf(a))return decodeURIComponent(d.substring(a.length,d.length))}return null},_fnDetectHeader:V,_fnGetUniqueThs:N,_fnScrollBarWidth:Qa,_fnApplyToChildren:C,_fnMap:p,_fnGetRowData:Y,
+_fnGetCellData:v,_fnSetCellData:F,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:L,_fnApplyColumnDefs:ta,_fnBindAction:Ra,_fnExtend:Ta,_fnCallbackReg:z,_fnCallbackFire:A,_fnJsonString:Wa,_fnRender:S,_fnNodeToColumnIndex:fa,_fnInfoMacros:ja,_fnBrowserDetect:Ua,_fnGetColumns:r};h.extend(j.ext.oApi,this.oApi);for(var sa in j.ext.oApi)sa&&(this[sa]=Va(sa));var ca=this;this.each(function(){var a=0,b,c,d;c=this.getAttribute("id");var i=!1,f=!1;if("table"!=this.nodeName.toLowerCase())D(null,0,"Attempted to initialise DataTables on a node which is not a table: "+
+this.nodeName);else{a=0;for(b=j.settings.length;a<b;a++){if(j.settings[a].nTable==this){if(e===n||e.bRetrieve)return j.settings[a].oInstance;if(e.bDestroy){j.settings[a].oInstance.fnDestroy();break}else{D(j.settings[a],0,"Cannot reinitialise DataTable.\n\nTo retrieve the DataTables object for this table, pass no arguments or see the docs for bRetrieve and bDestroy");return}}if(j.settings[a].sTableId==this.id){j.settings.splice(a,1);break}}if(null===c||""===c)this.id=c="DataTables_Table_"+j.ext._oExternConfig.iNextUnique++;
+var g=h.extend(!0,{},j.models.oSettings,{nTable:this,oApi:ca.oApi,oInit:e,sDestroyWidth:h(this).width(),sInstance:c,sTableId:c});j.settings.push(g);g.oInstance=1===ca.length?ca:h(this).dataTable();e||(e={});e.oLanguage&&pa(e.oLanguage);e=Ta(h.extend(!0,{},j.defaults),e);p(g.oFeatures,e,"bPaginate");p(g.oFeatures,e,"bLengthChange");p(g.oFeatures,e,"bFilter");p(g.oFeatures,e,"bSort");p(g.oFeatures,e,"bInfo");p(g.oFeatures,e,"bProcessing");p(g.oFeatures,e,"bAutoWidth");p(g.oFeatures,e,"bSortClasses");
+p(g.oFeatures,e,"bServerSide");p(g.oFeatures,e,"bDeferRender");p(g.oScroll,e,"sScrollX","sX");p(g.oScroll,e,"sScrollXInner","sXInner");p(g.oScroll,e,"sScrollY","sY");p(g.oScroll,e,"bScrollCollapse","bCollapse");p(g.oScroll,e,"bScrollInfinite","bInfinite");p(g.oScroll,e,"iScrollLoadGap","iLoadGap");p(g.oScroll,e,"bScrollAutoCss","bAutoCss");p(g,e,"asStripeClasses");p(g,e,"asStripClasses","asStripeClasses");p(g,e,"fnServerData");p(g,e,"fnFormatNumber");p(g,e,"sServerMethod");p(g,e,"aaSorting");p(g,
+e,"aaSortingFixed");p(g,e,"aLengthMenu");p(g,e,"sPaginationType");p(g,e,"sAjaxSource");p(g,e,"sAjaxDataProp");p(g,e,"iCookieDuration");p(g,e,"sCookiePrefix");p(g,e,"sDom");p(g,e,"bSortCellsTop");p(g,e,"iTabIndex");p(g,e,"oSearch","oPreviousSearch");p(g,e,"aoSearchCols","aoPreSearchCols");p(g,e,"iDisplayLength","_iDisplayLength");p(g,e,"bJQueryUI","bJUI");p(g,e,"fnCookieCallback");p(g,e,"fnStateLoad");p(g,e,"fnStateSave");p(g.oLanguage,e,"fnInfoCallback");z(g,"aoDrawCallback",e.fnDrawCallback,"user");
+z(g,"aoServerParams",e.fnServerParams,"user");z(g,"aoStateSaveParams",e.fnStateSaveParams,"user");z(g,"aoStateLoadParams",e.fnStateLoadParams,"user");z(g,"aoStateLoaded",e.fnStateLoaded,"user");z(g,"aoRowCallback",e.fnRowCallback,"user");z(g,"aoRowCreatedCallback",e.fnCreatedRow,"user");z(g,"aoHeaderCallback",e.fnHeaderCallback,"user");z(g,"aoFooterCallback",e.fnFooterCallback,"user");z(g,"aoInitComplete",e.fnInitComplete,"user");z(g,"aoPreDrawCallback",e.fnPreDrawCallback,"user");g.oFeatures.bServerSide&&
+g.oFeatures.bSort&&g.oFeatures.bSortClasses?z(g,"aoDrawCallback",P,"server_side_sort_classes"):g.oFeatures.bDeferRender&&z(g,"aoDrawCallback",P,"defer_sort_classes");e.bJQueryUI?(h.extend(g.oClasses,j.ext.oJUIClasses),e.sDom===j.defaults.sDom&&"lfrtip"===j.defaults.sDom&&(g.sDom='<"H"lfr>t<"F"ip>')):h.extend(g.oClasses,j.ext.oStdClasses);h(this).addClass(g.oClasses.sTable);if(""!==g.oScroll.sX||""!==g.oScroll.sY)g.oScroll.iBarWidth=Qa();g.iInitDisplayStart===n&&(g.iInitDisplayStart=e.iDisplayStart,
+g._iDisplayStart=e.iDisplayStart);e.bStateSave&&(g.oFeatures.bStateSave=!0,Sa(g,e),z(g,"aoDrawCallback",ra,"state_save"));null!==e.iDeferLoading&&(g.bDeferLoading=!0,a=h.isArray(e.iDeferLoading),g._iRecordsDisplay=a?e.iDeferLoading[0]:e.iDeferLoading,g._iRecordsTotal=a?e.iDeferLoading[1]:e.iDeferLoading);null!==e.aaData&&(f=!0);""!==e.oLanguage.sUrl?(g.oLanguage.sUrl=e.oLanguage.sUrl,h.getJSON(g.oLanguage.sUrl,null,function(a){pa(a);h.extend(true,g.oLanguage,e.oLanguage,a);ba(g)}),i=!0):h.extend(!0,
+g.oLanguage,e.oLanguage);null===e.asStripeClasses&&(g.asStripeClasses=[g.oClasses.sStripeOdd,g.oClasses.sStripeEven]);b=g.asStripeClasses.length;g.asDestroyStripes=[];if(b){c=!1;d=h(this).children("tbody").children("tr:lt("+b+")");for(a=0;a<b;a++)d.hasClass(g.asStripeClasses[a])&&(c=!0,g.asDestroyStripes.push(g.asStripeClasses[a]));c&&d.removeClass(g.asStripeClasses.join(" "))}c=[];a=this.getElementsByTagName("thead");0!==a.length&&(V(g.aoHeader,a[0]),c=N(g));if(null===e.aoColumns){d=[];a=0;for(b=
+c.length;a<b;a++)d.push(null)}else d=e.aoColumns;a=0;for(b=d.length;a<b;a++)e.saved_aoColumns!==n&&e.saved_aoColumns.length==b&&(null===d[a]&&(d[a]={}),d[a].bVisible=e.saved_aoColumns[a].bVisible),o(g,c?c[a]:null);ta(g,e.aoColumnDefs,d,function(a,b){m(g,a,b)});a=0;for(b=g.aaSorting.length;a<b;a++){g.aaSorting[a][0]>=g.aoColumns.length&&(g.aaSorting[a][0]=0);var k=g.aoColumns[g.aaSorting[a][0]];g.aaSorting[a][2]===n&&(g.aaSorting[a][2]=0);e.aaSorting===n&&g.saved_aaSorting===n&&(g.aaSorting[a][1]=
+k.asSorting[0]);c=0;for(d=k.asSorting.length;c<d;c++)if(g.aaSorting[a][1]==k.asSorting[c]){g.aaSorting[a][2]=c;break}}P(g);Ua(g);a=h(this).children("caption").each(function(){this._captionSide=h(this).css("caption-side")});b=h(this).children("thead");0===b.length&&(b=[l.createElement("thead")],this.appendChild(b[0]));g.nTHead=b[0];b=h(this).children("tbody");0===b.length&&(b=[l.createElement("tbody")],this.appendChild(b[0]));g.nTBody=b[0];g.nTBody.setAttribute("role","alert");g.nTBody.setAttribute("aria-live",
+"polite");g.nTBody.setAttribute("aria-relevant","all");b=h(this).children("tfoot");if(0===b.length&&0<a.length&&(""!==g.oScroll.sX||""!==g.oScroll.sY))b=[l.createElement("tfoot")],this.appendChild(b[0]);0<b.length&&(g.nTFoot=b[0],V(g.aoFooter,g.nTFoot));if(f)for(a=0;a<e.aaData.length;a++)H(g,e.aaData[a]);else ua(g);g.aiDisplay=g.aiDisplayMaster.slice();g.bInitialised=!0;!1===i&&ba(g)}});ca=null;return this};j.fnVersionCheck=function(e){for(var h=function(e,h){for(;e.length<h;)e+="0";return e},m=j.ext.sVersion.split("."),
+e=e.split("."),k="",n="",l=0,t=e.length;l<t;l++)k+=h(m[l],3),n+=h(e[l],3);return parseInt(k,10)>=parseInt(n,10)};j.fnIsDataTable=function(e){for(var h=j.settings,m=0;m<h.length;m++)if(h[m].nTable===e||h[m].nScrollHead===e||h[m].nScrollFoot===e)return!0;return!1};j.fnTables=function(e){var o=[];jQuery.each(j.settings,function(j,k){(!e||!0===e&&h(k.nTable).is(":visible"))&&o.push(k.nTable)});return o};j.version="1.9.4";j.settings=[];j.models={};j.models.ext={afnFiltering:[],afnSortData:[],aoFeatures:[],
+aTypes:[],fnVersionCheck:j.fnVersionCheck,iApiIndex:0,ofnSearch:{},oApi:{},oStdClasses:{},oJUIClasses:{},oPagination:{},oSort:{},sVersion:j.version,sErrMode:"alert",_oExternConfig:{iNextUnique:0}};j.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};j.models.oRow={nTr:null,_aData:[],_aSortData:[],_anHidden:[],_sRowStripe:""};j.models.oColumn={aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bUseRendered:null,bVisible:null,_bAutoType:!0,fnCreatedCell:null,fnGetData:null,
+fnRender:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};j.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,
+bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollAutoCss:!0,bScrollCollapse:!1,bScrollInfinite:!1,bServerSide:!1,bSort:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCookieCallback:null,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(e){if(1E3>e)return e;for(var h=e+"",e=h.split(""),j="",h=h.length,k=0;k<h;k++)0===k%3&&0!==k&&(j=this.oLanguage.sInfoThousands+j),j=e[h-k-1]+j;return j},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,
+fnRowCallback:null,fnServerData:function(e,j,m,k){k.jqXHR=h.ajax({url:e,data:j,success:function(e){e.sError&&k.oApi._fnLog(k,0,e.sError);h(k.oInstance).trigger("xhr",[k,e]);m(e)},dataType:"json",cache:!1,type:k.sServerMethod,error:function(e,h){"parsererror"==h&&k.oApi._fnLog(k,0,"DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.")}})},fnServerParams:null,fnStateLoad:function(e){var e=this.oApi._fnReadCookie(e.sCookiePrefix+e.sInstance),j;try{j=
+"function"===typeof h.parseJSON?h.parseJSON(e):eval("("+e+")")}catch(m){j=null}return j},fnStateLoadParams:null,fnStateLoaded:null,fnStateSave:function(e,h){this.oApi._fnCreateCookie(e.sCookiePrefix+e.sInstance,this.oApi._fnJsonString(h),e.iCookieDuration,e.sCookiePrefix,e.fnCookieCallback)},fnStateSaveParams:null,iCookieDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iScrollLoadGap:100,iTabIndex:0,oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},
+oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sInfoThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},j.models.oSearch),sAjaxDataProp:"aaData",
+sAjaxSource:null,sCookiePrefix:"SpryMedia_DataTables_",sDom:"lfrtip",sPaginationType:"two_button",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET"};j.defaults.columns={aDataSort:null,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bUseRendered:!0,bVisible:!0,fnCreatedCell:null,fnRender:null,iDataSort:-1,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};j.models.oSettings={oFeatures:{bAutoWidth:null,
+bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortClasses:null,bStateSave:null},oScroll:{bAutoCss:null,bCollapse:null,bInfinite:null,iBarWidth:0,iLoadGap:null,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1},aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aoColumns:[],aoHeader:[],aoFooter:[],asDataSearch:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:null,
+asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,sPaginationType:"two_button",iCookieDuration:0,sCookiePrefix:"",fnCookieCallback:null,aoStateSave:[],aoStateLoad:[],
+oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iDisplayEnd:10,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsTotal,10):this.aiDisplayMaster.length},
+fnRecordsDisplay:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsDisplay,10):this.aiDisplay.length},fnDisplayEnd:function(){return this.oFeatures.bServerSide?!1===this.oFeatures.bPaginate||-1==this._iDisplayLength?this._iDisplayStart+this.aiDisplay.length:Math.min(this._iDisplayStart+this._iDisplayLength,this._iRecordsDisplay):this._iDisplayEnd},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null};j.ext=h.extend(!0,{},j.models.ext);h.extend(j.ext.oStdClasses,
+{sTable:"dataTable",sPagePrevEnabled:"paginate_enabled_previous",sPagePrevDisabled:"paginate_disabled_previous",sPageNextEnabled:"paginate_enabled_next",sPageNextDisabled:"paginate_disabled_next",sPageJUINext:"",sPageJUIPrev:"",sPageButton:"paginate_button",sPageButtonActive:"paginate_active",sPageButtonStaticDisabled:"paginate_button paginate_button_disabled",sPageFirst:"first",sPagePrevious:"previous",sPageNext:"next",sPageLast:"last",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",
+sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",
+sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sFooterTH:"",sJUIHeader:"",sJUIFooter:""});h.extend(j.ext.oJUIClasses,j.ext.oStdClasses,{sPagePrevEnabled:"fg-button ui-button ui-state-default ui-corner-left",sPagePrevDisabled:"fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",sPageNextEnabled:"fg-button ui-button ui-state-default ui-corner-right",
+sPageNextDisabled:"fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",sPageJUINext:"ui-icon ui-icon-circle-arrow-e",sPageJUIPrev:"ui-icon ui-icon-circle-arrow-w",sPageButton:"fg-button ui-button ui-state-default",sPageButtonActive:"fg-button ui-button ui-state-default ui-state-disabled",sPageButtonStaticDisabled:"fg-button ui-button ui-state-default ui-state-disabled",sPageFirst:"first ui-corner-tl ui-corner-bl",sPageLast:"last ui-corner-tr ui-corner-br",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",
+sSortAsc:"ui-state-default",sSortDesc:"ui-state-default",sSortable:"ui-state-default",sSortableAsc:"ui-state-default",sSortableDesc:"ui-state-default",sSortableNone:"ui-state-default",sSortJUIAsc:"css_right ui-icon ui-icon-triangle-1-n",sSortJUIDesc:"css_right ui-icon ui-icon-triangle-1-s",sSortJUI:"css_right ui-icon ui-icon-carat-2-n-s",sSortJUIAscAllowed:"css_right ui-icon ui-icon-carat-1-n",sSortJUIDescAllowed:"css_right ui-icon ui-icon-carat-1-s",sSortJUIWrapper:"DataTables_sort_wrapper",sSortIcon:"DataTables_sort_icon",
+sScrollHead:"dataTables_scrollHead ui-state-default",sScrollFoot:"dataTables_scrollFoot ui-state-default",sFooterTH:"ui-state-default",sJUIHeader:"fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",sJUIFooter:"fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"});h.extend(j.ext.oPagination,{two_button:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)},k=!e.bJUI?'<a class="'+
+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sPrevious+'</a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sNext+"</a>":'<a class="'+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUIPrev+'"></span></a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUINext+'"></span></a>';h(j).append(k);var l=h("a",j),
+k=l[0],l=l[1];e.oApi._fnBindAction(k,{action:"previous"},n);e.oApi._fnBindAction(l,{action:"next"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_previous",l.id=e.sTableId+"_next",k.setAttribute("aria-controls",e.sTableId),l.setAttribute("aria-controls",e.sTableId))},fnUpdate:function(e){if(e.aanFeatures.p)for(var h=e.oClasses,j=e.aanFeatures.p,k,l=0,n=j.length;l<n;l++)if(k=j[l].firstChild)k.className=0===e._iDisplayStart?h.sPagePrevDisabled:h.sPagePrevEnabled,k=k.nextSibling,
+k.className=e.fnDisplayEnd()==e.fnRecordsDisplay()?h.sPageNextDisabled:h.sPageNextEnabled}},iFullNumbersShowPages:5,full_numbers:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,l=e.oClasses,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)};h(j).append('<a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageFirst+'">'+k.sFirst+'</a><a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPagePrevious+'">'+k.sPrevious+'</a><span></span><a tabindex="'+e.iTabIndex+'" class="'+
+l.sPageButton+" "+l.sPageNext+'">'+k.sNext+'</a><a tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageLast+'">'+k.sLast+"</a>");var t=h("a",j),k=t[0],l=t[1],r=t[2],t=t[3];e.oApi._fnBindAction(k,{action:"first"},n);e.oApi._fnBindAction(l,{action:"previous"},n);e.oApi._fnBindAction(r,{action:"next"},n);e.oApi._fnBindAction(t,{action:"last"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_first",l.id=e.sTableId+"_previous",r.id=e.sTableId+"_next",t.id=e.sTableId+"_last")},
+fnUpdate:function(e,o){if(e.aanFeatures.p){var m=j.ext.oPagination.iFullNumbersShowPages,k=Math.floor(m/2),l=Math.ceil(e.fnRecordsDisplay()/e._iDisplayLength),n=Math.ceil(e._iDisplayStart/e._iDisplayLength)+1,t="",r,B=e.oClasses,u,M=e.aanFeatures.p,L=function(h){e.oApi._fnBindAction(this,{page:h+r-1},function(h){e.oApi._fnPageChange(e,h.data.page);o(e);h.preventDefault()})};-1===e._iDisplayLength?n=k=r=1:l<m?(r=1,k=l):n<=k?(r=1,k=m):n>=l-k?(r=l-m+1,k=l):(r=n-Math.ceil(m/2)+1,k=r+m-1);for(m=r;m<=k;m++)t+=
+n!==m?'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButton+'">'+e.fnFormatNumber(m)+"</a>":'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButtonActive+'">'+e.fnFormatNumber(m)+"</a>";m=0;for(k=M.length;m<k;m++)u=M[m],u.hasChildNodes()&&(h("span:eq(0)",u).html(t).children("a").each(L),u=u.getElementsByTagName("a"),u=[u[0],u[1],u[u.length-2],u[u.length-1]],h(u).removeClass(B.sPageButton+" "+B.sPageButtonActive+" "+B.sPageButtonStaticDisabled),h([u[0],u[1]]).addClass(1==n?B.sPageButtonStaticDisabled:
+B.sPageButton),h([u[2],u[3]]).addClass(0===l||n===l||-1===e._iDisplayLength?B.sPageButtonStaticDisabled:B.sPageButton))}}}});h.extend(j.ext.oSort,{"string-pre":function(e){"string"!=typeof e&&(e=null!==e&&e.toString?e.toString():"");return e.toLowerCase()},"string-asc":function(e,h){return e<h?-1:e>h?1:0},"string-desc":function(e,h){return e<h?1:e>h?-1:0},"html-pre":function(e){return e.replace(/<.*?>/g,"").toLowerCase()},"html-asc":function(e,h){return e<h?-1:e>h?1:0},"html-desc":function(e,h){return e<
+h?1:e>h?-1:0},"date-pre":function(e){e=Date.parse(e);if(isNaN(e)||""===e)e=Date.parse("01/01/1970 00:00:00");return e},"date-asc":function(e,h){return e-h},"date-desc":function(e,h){return h-e},"numeric-pre":function(e){return"-"==e||""===e?0:1*e},"numeric-asc":function(e,h){return e-h},"numeric-desc":function(e,h){return h-e}});h.extend(j.ext.aTypes,[function(e){if("number"===typeof e)return"numeric";if("string"!==typeof e)return null;var h,j=!1;h=e.charAt(0);if(-1=="0123456789-".indexOf(h))return null;
+for(var k=1;k<e.length;k++){h=e.charAt(k);if(-1=="0123456789.".indexOf(h))return null;if("."==h){if(j)return null;j=!0}}return"numeric"},function(e){var h=Date.parse(e);return null!==h&&!isNaN(h)||"string"===typeof e&&0===e.length?"date":null},function(e){return"string"===typeof e&&-1!=e.indexOf("<")&&-1!=e.indexOf(">")?"html":null}]);h.fn.DataTable=j;h.fn.dataTable=j;h.fn.dataTableSettings=j.settings;h.fn.dataTableExt=j.ext};"function"===typeof define&&define.amd?define(["jquery"],L):jQuery&&!jQuery.fn.dataTable&&
+L(jQuery)})(window,document);
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-activate-tab.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-activate-tab.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-activate-tab.js	(revision 16045)
@@ -0,0 +1,10 @@
+$(document).ready(function(){
+
+/*
+Activate current tab, if any, but jump to top, if hash value contains '-top'
+*/
+
+  var anchor = window.location.hash.replace('-top','');
+  $('a[href="' + anchor + '"]').trigger('click');
+
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datatables.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datatables.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datatables.js	(revision 16045)
@@ -0,0 +1,32 @@
+$(document).ready(function(){
+
+  $('.dataTable').dataTable( {
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+  } );
+
+	$('.dataTableFiles').dataTable( {
+    "aaSorting": [[ 2, "desc" ]],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+	$('.dataTableManage').dataTable( {
+    "aaSorting": [[ 1, "asc" ]],
+    "aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+	$('.dataTableManageFiles').dataTable( {
+    "aaSorting": [[ 3, "desc" ]],
+    "aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }],
+    "iDisplayLength": 1000,
+    "iDisplayEnd": 1000,
+    "aLengthMenu": [ 50, 100, 1000, 5000 ],
+	} );
+
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datepicker.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datepicker.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-datepicker.js	(revision 16045)
@@ -0,0 +1,55 @@
+$(function() {
+  $( ".datepicker" ).datepicker({ dateFormat: 'yy-mm-dd',
+                                  duration: 'fast'
+  });
+});
+
+$(function() {
+  $( ".datepicker-le" ).datepicker({dateFormat: 'dd/mm/yy',
+	duration: 'fast'
+	});
+  });
+
+$(function() {
+  $( ".datepicker-de" ).datepicker({dateFormat: 'dd.mm.yy',
+	duration: 'fast'
+	});
+  });
+
+$(function() {
+  $( ".datepicker-us" ).datepicker({dateFormat: 'mm/dd/yy',
+	duration: 'fast'
+	});
+  });
+
+$(function() {
+  $( ".datepicker-year" ).datepicker({ dateFormat: 'yy-mm-dd',
+	duration: 'fast',
+	changeYear: true,
+	yearRange: '1900:2020'
+	});
+});
+
+$(function() {
+  $( ".datepicker-le-year" ).datepicker({dateFormat: 'dd/mm/yy',
+	duration: 'fast',
+	changeYear: true,
+	yearRange: '1900:2020'
+	});
+  });
+
+$(function() {
+  $( ".datepicker-de-year" ).datepicker({dateFormat: 'dd.mm.yy',
+	duration: 'fast',
+	changeYear: true,
+	yearRange: '1900:2020'
+	});
+  });
+
+$(function() {
+  $( ".datepicker-us-year" ).datepicker({dateFormat: 'mm/dd/yy',
+	duration: 'fast',
+	changeYear: true,
+	yearRange: '1900:2020'
+	});
+  });
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-filterByText.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-filterByText.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-filterByText.js	(revision 16045)
@@ -0,0 +1,33 @@
+jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
+  return this.each(function() {
+    var select = this;
+    var options = [];
+    $(select).find('option').each(function() {
+      options.push({value: $(this).val(), text: $(this).text()});
+    });
+    $(select).data('options', options);
+    $(textbox).bind('change keyup', function() {
+      var options = $(select).empty().scrollTop(0).data('options');
+      var search = $.trim($(this).val());
+      var regex = new RegExp(search,'gi');
+
+      $.each(options, function(i) {
+        var option = options[i];
+        if(option.text.match(regex) !== null) {
+          $(select).append(
+             $('<option>').text(option.text).val(option.value)
+          );
+        }
+      });
+      if (selectSingleMatch === true &&
+          $(select).children().length === 1) {
+        $(select).children().get(0).selected = true;
+      }
+    });
+  });
+};
+
+
+$(function() {
+    $('#filteredselect').filterByText($('#filterbox'), true);
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-functions.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-functions.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-functions.js	(revision 16045)
@@ -0,0 +1,22 @@
+var RecaptchaOptions = {
+    theme : 'white'
+ };
+
+function renderCurrentYear() {
+	var mydate=new Date()
+	var year=mydate.getYear()
+	if (year < 1000)
+	year+=1900
+	var day=mydate.getDay()
+	var month=mydate.getMonth()
+	var daym=mydate.getDate()
+	if (daym<10)
+	daym="0"+daym
+	document.write(year)
+}
+
+function toggle(source, name) {
+  checkboxes = document.getElementsByName(name);
+  for(var i in checkboxes)
+    checkboxes[i].checked = source.checked;
+}
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scroll.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scroll.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scroll.js	(revision 16045)
@@ -0,0 +1,45 @@
+(function($){
+	$.fn.BottomTop = function(pos,cssClass,anchor) {
+
+		var easingType = 'easeInOutCubic';
+		var min = -1;
+		var inDelay = 500;
+		var outDelay = 500;
+		var scrollSpeed = 750;
+		var containerIDhash = '#' + pos;
+
+		// Auto Link
+		$('body').append('<a href="#'+anchor+'" id="'+pos+'" class="'+cssClass+'" />');
+    if (anchor == 'bottom') {
+  		$(containerIDhash).hide().click(function(){
+  			$('html, body').animate({scrollTop:$(bottom).offset().top}, scrollSpeed, easingType);
+  			return false;
+  		})
+      }
+    if (anchor == '') {
+  		$(containerIDhash).hide().click(function(){
+  			$('html, body').animate({scrollTop:0}, scrollSpeed, easingType);
+  			return false;
+  		})
+      }
+
+		$(window).scroll(function() {
+			var sd = $(window).scrollTop();
+
+			if ( sd > min )
+				$(containerIDhash).fadeIn(inDelay);
+			else
+				$(containerIDhash).fadeOut(Outdelay);
+		});
+
+};
+})(jQuery);
+
+
+$(document).ready(function(){
+	$('.top').BottomTop('toTop-right','scroll-up','');
+});
+
+$(document).ready(function(){
+	$('.bottom').BottomTop('toBottom-right','scroll-down','bottom');
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scrollto.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scrollto.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-scrollto.js	(revision 16045)
@@ -0,0 +1,12 @@
+/* After loading the page, softly scroll to the #kofa-scroll-target tag,
+   if it exists */
+$(document).ready(function (){
+    var scroll_target = $('#kofa-scroll-target');
+    if (scroll_target.length) {
+	$('html,body').animate(
+            {
+		scrollTop: scroll_target.offset().top
+		    - $(window).height() + 2 * scroll_target.height()
+            })
+    }
+});
Index: main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-tooltip.js
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-tooltip.js	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/static/js/kofa-tooltip.js	(revision 16045)
@@ -0,0 +1,3 @@
+$(function () {
+  $("[data-toggle=tooltip]").tooltip();
+});
Index: main/waeup.kofa/branches/uli-py3/layout/theme.html
===================================================================
--- main/waeup.kofa/branches/uli-py3/layout/theme.html	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/layout/theme.html	(revision 16045)
@@ -0,0 +1,614 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Base · Twitter Bootstrap</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+
+    <!-- stylesheets -->
+    <link href="static/bootstrap/css/bootstrap.min.css"
+          rel="stylesheet">
+    <link href="static/bootstrap/css/bootstrap-theme.min.css"
+          rel="stylesheet">
+    <link href="static/css/jquery.dataTables.css"
+          rel="stylesheet">
+    <link href="static/css/jquery-ui.css"
+          rel="stylesheet">
+    <link href="static/css/base.css" rel="stylesheet">
+    <link href="static/css/file-upload.css" rel="stylesheet">
+
+    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
+      <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
+      <link href="static/css/IE8.css" rel="stylesheet">
+    <![endif]-->
+
+    <script src="static/js/kofa-functions.js"></script>
+
+    <!-- icons -->
+    <link rel="shortcut icon" href="static/ico/favicon.ico">
+
+
+  </head>
+
+  <body id="kofa-body">
+
+    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+      <div class="navbar-header">
+        <button type="button" class="navbar-toggle" data-toggle="collapse"
+                  data-target="#kofa-primary-nav">
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+          <span class="icon-bar"></span>
+        </button>
+        <a class="navbar-brand active kofa-brand-link" href="/">WAeUP.Kofa</a>
+      </div>
+      <div class="navbar-collapse collapse"
+           id="kofa-primary-nav">
+        <ul class="nav navbar-nav kofa-navbar-left">
+          <li class="active"><a href="#">Academics</a></li>
+          <li><a href="#about">Applicants</a></li>
+          <li><a href="#contact">Students</a></li>
+          <li><a href="#contact">Hostels</a></li>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle"
+               data-toggle="dropdown">My Data
+               <b class="caret"></b>
+            <ul class="dropdown-menu">
+              <li><a href="#">Base Data</a></li>
+              <li><a href="#">Clearance Data</a></li>
+              <li><a href="#">Personal Data</a></li>
+              <li><a href="#">History</a></li>
+            </ul>
+          </li>
+        </ul>
+
+        <ul class="nav navbar-nav navbar-right kofa-navbar-right">
+          <li class="dropdown kofa-langselect">
+            <a href="#" class="dropdown-toggle"
+               data-toggle="dropdown">
+               <img src="static/img/earth.png" />
+            <ul class="dropdown-menu">
+              <li class="kofa-language"><a href="#">English</a></li>
+              <li class="kofa-language"><a href="#">Français</a></li>
+              <li class="kofa-language"><a href="#">Deutsch</a></li>
+              <li class="kofa-language"><a href="#">Hausa</a></li>
+              <li class="kofa-language"><a href="#">Yoruba</a></li>
+              <li class="kofa-language"><a href="#">Igbo</a></li>
+            </ul>
+          </li>
+          <li><a href="#">Contact</a></li>
+          <li><a href="#">Logout</a></li>
+        </ul>
+      </div><!--/.nav-collapse -->
+    </div><!-- /.navbar -->
+
+    <div class="container">
+
+      <div class="row">
+        <div class="col-md-12 container kofa-maintenance">
+          <div class="well">
+            <div class="maintenance-warning">
+                M A I N T E N A N C E &nbsp;&nbsp;&nbsp; M O D E
+            </div> <!-- /maintenance-warning -->
+          </div> <!-- /well -->
+        </div> <!-- /kofa-maintenance -->
+      </div>
+
+      <div class="row">
+        <div class="col-md-12 container kofa-breadcrumbs-box-long">
+          <div class="well">
+            <ol class="breadcrumb kofa-breadcrumbs">
+              <li>
+                <a href="#">
+                  Home
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 1
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 2
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 3
+                </a>
+              </li>
+            </ol><!-- /kofa-breadcrumbs -->
+          </div> <!-- /well -->
+        </div> <!-- /kofa-breadcrumbs-box-long -->
+      </div>
+
+      <div class="row">
+        <div class="col-md-8 container kofa-breadcrumbs-box-short">
+          <div class="well">
+            <ol class="breadcrumb kofa-breadcrumbs">
+              <li>
+                <a href="#">
+                  Home
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 1
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 2
+                </a>
+              </li>
+              <li>
+                <a href="#">
+                  Subtopic 3
+                </a>
+              </li>
+            </ol><!-- /kofa-breadcrumbs -->
+          </div> <!-- /well -->
+        </div> <!-- /kofa-breadcrumbs-box-short -->
+
+        <div class="col-md-4 container kofa-student-status-box">
+          <div class="well">
+            <div class="wfstatus">
+                K1000000 &bull; quite long student status
+            </div> <!-- /wfstatus -->
+          </div> <!-- /well -->
+        </div> <!-- /kofa-student-status-box -->
+
+      </div> <!-- /row -->
+
+      <div class="row">
+        <div class="col-md-2 kofa-sidebar">
+          <div class="sidebar-nav well">
+            <div>
+              <div class="kofa-sidebar-title">Student Name</div>
+              <ul class="nav">
+                <li><a href="#">Base Data</a></li>
+                <li><a href="#">Clearance Data</a></li>
+                <li><a href="#">Personal Data</a></li>
+                <li><a href="#">Study Course</a></li>
+                <li><a href="#">Payments</a></li>
+                <li><a href="#">Accommodation</a></li>
+                <li><a href="#">History</a></li>
+              </ul>
+            </div>
+            <div>
+              <div class="kofa-sidebar-title">Manager Name</div>
+              <ul class="nav">
+                <li><a href="#">Portal Configuration</a></li>
+                <li><a href="#">Portal Users</a></li>
+                <li><a href="#">Data Center</a></li>
+                <li><a href="#">Reports</a></li>
+                <li><a href="#">Access Codes</a></li>
+                <li><a href="#">My Preferences</a></li>
+                <li><a href="#">My Roles</a></li>
+              </ul>
+            </div>
+          </div> <!-- /well -->
+        </div> <!-- /kofa-sidebar -->
+
+        <div class="col-md-10 content kofa-content-narrow">
+          <div class="well">
+            <div class="kofa-content">
+              <div class="actionbar">
+                <a class="btn btn-default" href="">
+                  <img src="static/img/actionicon_modify.png"
+                       alt="Manage" />
+                       Manage
+                </a>
+                <a class="btn btn-default" href="">
+                  <img src="static/img/actionicon_trigtrans.png"
+                       alt="Trigger transition" />
+                       Trigger transition
+                </a>
+              </div>
+              <div class="alert alert-danger">
+                  There were errors ...
+              </div>
+              <div class="alert alert-warning">
+                  Be careful ...
+              </div>
+              <div class="alert alert-success">
+                  You have successfully ...
+              </div>
+              <h1>WAeUP.Kofa Theme</h1>
+
+              <p class="kofa-content-desc">
+                A sample Diazo theme for WAeUP.Kofa
+                <br />
+                Fusce dapibus, tellus ac cursus commodo.
+              </p>
+              <p>
+                Donec id elit non mi porta gravida at eget metus. Fusce
+                dapibus, tellus ac cursus commodo, tortor mauris
+                condimentum nibh, ut fermentum massa justo sit amet
+                risus. Etiam porta sem malesuada magna mollis
+                euismod. Donec sed odio dui.
+              </p>
+              <img src="static/img/bg.png" />
+
+              <!-- Tabs... -->
+              <br /><br />
+              <h3>Sample Tabs</h3>
+              <ul id="tabs" class="tabs nav nav-tabs" data-tabs="tabs">
+                <li class="active">
+                  <a href="#tab-1" data-toggle="tab">Tab 1</a>
+                </li>
+                <li class="">
+                  <a href="#tab-2" data-toggle="tab">Tab 2</a>
+                </li>
+                <li class="">
+                  <a href="#tab-3" data-toggle="tab">Tab 3</a>
+                </li>
+              </ul>
+              <div class="tab-content">
+                <div id="tab-1" class="tab-pane active">
+                  <br />
+                  <p>
+                    Tab 1 content...<br />  Donec id elit non mi porta
+                    gravida at eget metus. Fusce dapibus, tellus ac
+                    cursus commodo.
+                  </p>
+                </div>
+                <div id="tab-2" class="tab-pane">
+                  <br />
+                  <p>
+                    Tab 2 content...<br />  Tortor mauris condimentum
+                    nibh, ut fermentum massa justo sit amet
+                    risus. Etiam porta sem malesuada.
+                  </p>
+                </div>
+                <div id="tab-3" class="tab-pane">
+                  <br />
+                  <p>
+                    Tab 3 content...<br /> Magna mollis euismod. Donec
+                    sed odio dui.
+                  </p>
+                </div>
+
+                <br>
+
+                <h3>Sample Tooltip</h3>
+                <br>Move the mouse over the button:
+                <div class="btn btn-default" data-toggle="tooltip"
+                     title="This is a tooltip">Sample Tooltip
+                </div>
+
+                <br><br>
+
+                <h3>File Upload</h3>
+                <br>
+                <a href="#">Birth Certificate Scan</a>
+                <input type="submit"
+                       value="Delete file"
+                       class="btn btn-danger" />
+                <div class="btn btn-default btn-file">
+                  Select new file&hellip;
+                  <input type="file"/>
+                </div>
+                <input type="submit"
+                       value="Upload selected file"
+                       class="btn btn-primary" />
+                (10 kB max.)
+                <br><br>
+
+                <div class="input-group">
+                  <div class="input-group-btn">
+                    <div class="btn btn-default btn-file">
+                      Select new file&hellip;
+                      <input type="file">
+                    </div>
+                  </div>
+                  <input type="text" class="form-control" readonly>
+                  <div class="input-group-btn">
+                    <input type="submit"
+                           value="Upload selected file"
+                           class="btn btn-primary" />
+                    (10 kB max.)
+                  </div>
+                </div>
+
+                <br><br>
+
+                <h3>Filtered Select Box</h3>
+                <br>
+                <input id="filterbox" class="kofa-filterbox form-control half"
+                       placeholder="Enter search text, then select item below."
+                       type="text" />
+                <br />
+                <select id="filteredselect" class="form-control half" size=2>
+                  <option value="1">1</option>
+                  <option value="1234567890">1234567890</option>
+                  <option value="better">better</option>
+                  <option value="world">world</option>
+                </select>
+
+              </div><!-- /tab-content -->
+            </div> <!-- /kofa-content -->
+          </div> <!-- /well -->
+        </div> <!-- /kofa-content-narrow -->
+      </div> <!-- /row -->
+
+      <div class="row">
+        <div class="col-md-12 content kofa-content-wide">
+          <div class="well kofa-content">
+            <div class="actionbar">
+              <a class="btn btn-default" href="">
+                <img src="static/img/actionicon_modify.png"
+                     alt="Manage" />
+                     Manage
+              </a>
+              <a class="btn btn-default" href="">
+                <img src="static/img/actionicon_trigtrans.png"
+                     alt="Trigger transition" />
+                     Trigger transition
+              </a>
+            </div>
+            <div class="hero-unit">
+              <h1>Hello there!</h1>
+              <p>this is...</p>
+            </div>
+            <div>...content that spans the whole page.</div>
+
+            <br><br>
+            <h3>Accordions</h3>
+            <div class="panel-group" id="accordion" role="tablist"
+                 aria-multiselectable="true">
+              <div class="panel panel-default">
+                <div class="panel-heading" role="tab" id="headingTwo">
+                  <h4 class="panel-title">
+                    <a class="collapsed" data-toggle="collapse"
+                       data-parent="#accordion" href="#collapseTwo"
+                       aria-expanded="false" aria-controls="collapseTwo">
+                      Collapsible Group
+                    </a>
+                  </h4>
+                </div>
+                <div id="collapseTwo" class="panel-collapse collapse"
+                     role="tabpanel" aria-labelledby="headingTwo">
+                  <div class="panel-body">
+                    Anim pariatur cliche reprehenderit, enim eiusmod high life
+                    terry richardson ad squid. 3 wolf moon officia aute, non
+                    skateboard dolor brunch. Food truck quinoa laborum eiusmod.
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <br><br>
+            <h3>A Sample Data Table</h3>
+            <div class="table-responsive">
+              <table id="#datatable" class="table table-striped dataTable">
+                <thead>
+                  <tr>
+                    <th>Id</th>
+                    <th>Name</th>
+                    <th>Portal Roles</th>
+                    <th>Local Roles</th>
+                    <th></th>
+                  </tr>
+                </thead><tbody><tr><td>admin</td>
+                    <td>John</td>
+                    <td nowrap="nowrap">Portal Manager <br />
+                      Academics Officer (view only)
+                    </td>
+                    <td>Owner</td>
+                    <td class="text-right">
+                      <form method="post">
+                        <input type="hidden" name="userid" value="admin" />
+                        <input class="btn btn-primary btn-sm" type="submit"
+                               name="manage" value="Manage" />
+                        <input class="btn btn-default btn-sm" type="submit"
+                               name="delete" value="Remove" />
+                      </form>
+                    </td>
+                  </tr>
+                  <tr>
+                    <td>tester</td>
+                    <td>Vitali</td>
+                    <td nowrap="nowrap">Academics Officer (view only)</td>
+                    <td>Owner</td>
+                    <td class="text-right">
+                      <form method="post">
+                        <input type="hidden" name="userid" value="your_id" />
+                        <input class="btn btn-sm btn-primary" type="submit"
+                               name="manage" value="Manage" />
+                        <input class="btn btn-sm btn-default" type="submit"
+                               name="delete" value="Remove" />
+                      </form>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+
+            <br><br>
+            <h3>A Sample Inline Form</h3>
+            <form method="POST" class="form-inline">
+            <br />
+              <div class="form-group">
+                <input class="btn btn-primary" type="submit" name="search"
+                  value="Find student(s)" />
+              </div>
+              <div class="form-group">
+                <select name="searchtype" class="form-control">
+                  <option value="student_id">with id</option>
+                  <option value="fullname">with name</option>
+                  <option value="reg_number">with registration number</option>
+                  <option value="matric_number">with matric number</option>
+                  <option value="current_session">in session</option>
+                  <option value="depcode">in department</option>
+                  <option value="current_mode">in study mode</option>
+                  <option value="suspended">
+                    with deactivated account (search term omitted)
+                  </option>
+                  <option value="transcript">
+                    who requested transcript (search term omitted)
+                  </option>
+                </select>
+              </div>
+              <div class="form-group">
+                <input type="text" class="form-control" name="searchterm" />
+              </div>
+            </form>
+
+            <br><br>
+            <h3>A Sample Display Form Table</h3>
+            <table class="form-table">
+              <tbody>
+                <tr>
+                  <td class="fieldname">Code:</td>
+                  <td>app2012</td>
+                </tr>
+                <tr>
+                 <td class="fieldname">Application Target:</td>
+                  <td>General Studies</td>
+                </tr>
+                 <tr>
+                  <td class="fieldname">
+                    Application Mode (Donec id elit non mi porta gravida.):
+                  </td>
+                  <td>Create Application Records</td>
+                </tr>
+              </tbody>
+            </table>
+
+            <br><br>
+            <h3>A Sample Edit/Manage Form Table</h3>
+            <table class="form-table">
+              <tbody>
+                <tr>
+                  <td class="fieldname">Code:</td>
+                  <td>app2012</td>
+                </tr>
+                <tr>
+                  <td class="fieldname">* Title:</td>
+                  <td>
+                    <input class="textType" id="form.title" name="form.title"
+                           size="20" type="text"
+                           value="General Studies 2012/2013" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">Any phone number:</td>
+                  <td>
+                    <select id="form.phone.country" name="form.phone.country"
+                              size="1" class="phone-input">
+                      <option value="+234">Nigeria (+234)</option>
+                      <option value="+49">Germany (+49)</option>
+                      <option value="+1">U.S. (+1)</option>
+                    </select>-
+                    <input class=" phone-input textType" id="form.phone.area"
+                           name="form.phone.area" size="20" type="text"
+                           value="" />-
+                    <input class=" phone-input textType" id="form.phone.ext"
+                           name="form.phone.ext" size="20" type="text" value="" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">Any text field:</td>
+                  <td>
+                      <textarea id="any_id"
+                          name="any_name" rows="5">Placeholder text</textarea>
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">*
+                      Application Mode
+                      (A select field coming from zope.formlib):
+                  </td>
+                  <td>
+                    <div class="value">
+                      <select id="form.mode" name="form.mode" size="1">
+                        <option selected="selected" value="create">
+                          Create Application Records
+                        </option>
+                        <option value="update">Update Application Records
+                        </option>
+                      </select>
+                    </div>
+                    <input name="form.mode-empty-marker"
+                           type="hidden" value="1" />
+                  </td>
+                </tr>
+                <tr>
+                  <td class="fieldname">A datepicker field:</td>
+                  <td>
+                    <input class="datepicker-le-year textType"
+                                 id="form.any_date" name="form.any_date"
+                                 size="20" type="text" value="" />
+                  </td>
+                </tr>
+
+                <tr>
+                  <td class="fieldname">
+                      A select field in Kofa pagetemplates:
+                  </td>
+                  <td>
+                      <select id="any_id" name="any_name" size="1"
+                              class="form-control half">
+                        <option selected="selected" value="create">
+                            Create Application Records
+                        </option>
+                        <option value="update">
+                            Update Application Records
+                        </option>
+                      </select>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+            <br>
+            <div class="documentation">
+              <a href="#" target="_blank">
+                Kofa Docs for this page
+              </a>
+            </div>
+          </div> <!-- /well kofa-content-->
+        </div> <!-- /kofa-content-wide -->
+      </div> <!-- /row -->
+      <div id="bottom" class="footer">
+        <p class="credit muted text-right">
+          Kofa - Student Management System<br />
+          Copyright &copy; WAeUP Group
+          <script type="text/javascript">
+          renderCurrentYear();
+          </script>
+        </p>
+      </div>
+    </div> <!-- /container -->
+
+    <script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
+    <script src="static/bootstrap/js/bootstrap.min.js"></script>
+    <script src="static/js/kofa-scroll.js"></script>
+    <script src="static/js/kofa-activate-tab.js"></script>
+    <script src="static/js/kofa-scrollto.js"></script>
+    <script src="static/js/file-upload.js"></script>
+    <script class="kofa-script-tooltip"
+        src="static/js/kofa-tooltip.js">
+    </script>
+    <script class="kofa-script-datatable"
+        src="static/js/jquery.dataTables.min.js">
+    </script>
+    <script class="kofa-script-datatable"
+        src="static/js/kofa-datatables.js">
+    </script>
+    <script class="jquery-ui"
+        src="static/js/jquery-ui.min.js">
+    </script>
+    <script class="kofa-script-datepicker"
+        src="static/js/kofa-datepicker.js">
+    </script>
+    <script class="kofa-filterByText"
+        src="static/js/kofa-filterByText.js">
+    </script>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/setup.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/setup.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/setup.py	(revision 16045)
@@ -0,0 +1,140 @@
+import os
+from setuptools import setup, find_packages
+
+version = '1.6.1.dev0'
+
+install_requires =[
+    'setuptools',
+    #'gp.fileupload',
+    'grok',
+    'grokcore.startup',
+    'grokui.admin',
+    'hurry.query',
+    'hurry.jquery',
+    'hurry.jqueryui',
+    'hurry.workflow >= 0.11',
+    # Add extra requirements here
+    'docutils', # For RST-processing...
+    'zope.xmlpickle',
+    'hurry.file',
+    'hurry.zoperesource',
+    'zc.sourcefactory',
+    'megrok.layout',
+    'reportlab',
+    'Pillow',
+    'psutil',
+    'unicodecsv',
+    'zope.app.authentication', # BBB: During switch to grok 1.1
+    'zope.app.file',
+    'zope.app.testing',        # XXX: test_permissions needs this
+    'zope.app.undo',
+    'zope.file',
+    'zope.interface >= 3.6.0',
+    'zope.testbrowser',        # XXX: test_permissions needs this
+    'zope.i18n',
+    'zope.mimetype',
+    'zope.errorview',
+    'zope.schema >= 3.8.0',
+    'zope.sendmail',
+    'ulif.loghandlers',
+    'zc.async[z3]',
+    'z3c.evalexception',
+    ],
+
+diazo_require = [
+    'diazo',
+    'webob',
+    ]
+
+# Having beaker installed additionally is a feature very recommended
+# for production use. The default buildout includes beaker for tests,
+# start scripts, and other parts that can benefit from it. The windows
+# buildout does not include it due to compiling problems with the
+# beaker package.
+beaker_require = [
+    'dolmen.beaker',
+    ]
+
+tests_require = [
+    'z3c.testsetup',
+    'zope.app.testing',
+    'zope.testbrowser',
+    'zope.testing',
+    'unittest2',
+    ]
+
+docs_require = [
+    'Sphinx',
+    'collective.recipe.sphinxbuilder',
+    'docutils',
+    'roman',
+    'repoze.sphinx.autointerface',
+    ]
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('README.txt')
+    + '\n\n'
+    + read('CHANGES.txt')
+    + '\n\n'
+    + 'Download\n'
+    + '********\n'
+    )
+
+setup(name = 'waeup.kofa',
+      version = version,
+      description = "A student online information and  registration portal",
+      long_description = long_description,
+
+      keywords = "portal waeup kofa student university registration grok zope",
+      # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      classifiers = [
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Intended Audience :: Education',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Programming Language :: Python',
+        'Operating System :: POSIX',
+        'Operating System :: POSIX :: Linux',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 2 :: Only',
+        'Programming Language :: Python :: Implementation :: CPython',
+        'Framework :: Zope3',
+        'Topic :: Education',
+        'Topic :: Internet :: WWW/HTTP',
+        ],
+      author = "The WAeUP team.",
+      author_email = "de@waeup.org",
+      url = "http://www.waeup.org/",
+      license = "GPL",
+      package_dir = {'': 'src'},
+      packages= find_packages('src'),
+      namespace_packages = ['waeup',],
+      include_package_data = True,
+      zip_safe = False,
+      install_requires = install_requires,
+      tests_require = tests_require,
+      extras_require = dict(
+        test = tests_require,
+        docs = docs_require,
+        beaker = beaker_require,
+        diazo = diazo_require,
+        ),
+      entry_points="""
+      # Add entry points here
+      #[hurry.resource.libraries]
+      #waeup_kofa = waeup.kofa.browser.resources:waeup_kofa
+      [console_scripts]
+      kofa-debug = grokcore.startup:interactive_debug_prompt
+      kofactl = grokcore.startup:zdaemon_controller
+      analyze = waeup.kofa.maintenance:db_analyze
+      fsdiff = waeup.kofa.maintenance:db_diff
+      [paste.app_factory]
+      main = waeup.kofa.startup:env_app_factory
+      debug = waeup.kofa.startup:env_debug_app_factory
+
+      """,
+      )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/__init__.py	(revision 16045)
@@ -0,0 +1,7 @@
+# namespace package boilerplate
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError, e:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/__init__.py	(revision 16045)
@@ -0,0 +1,13 @@
+import grok
+from waeup.kofa.interfaces import IKofaObject
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations
+
+class KofaAttributeAnnotations(AttributeAnnotations, grok.Adapter):
+    """An adapter to IAnnotations for any KofaObject.
+
+    Providing this adapter, each Kofa object becomes (attribute)
+    annotatable.
+    """
+    grok.provides(IAnnotations)
+    grok.context(IKofaObject)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/__init__.py	(revision 16045)
@@ -0,0 +1,33 @@
+"""
+Access codes are special objects used for identifying users. Each
+access code has a portal-wide unique number.
+
+Main content components are defined in
+:mod:`waeup.kofa.accesscodes.accesscode`. This subpackage also
+provides a _catalog_ to lookup any access code quickly and a
+_workflow_ to guarantee consistent states and transitions when dealing
+with it.
+
+We also provide UI components to generate, archive, import and
+reimport access codes or batches thereof. These parts are defined in
+:mod:`waeup.kofa.accesscodes.browser`.
+
+The interfaces of this subpackage are defined in
+:mod:`waeup.kofa.accesscodes.interfaces`.
+"""
+from waeup.kofa.accesscodes.accesscode import (
+    get_access_code, invalidate_accesscode, disable_accesscode,
+    reenable_accesscode, create_accesscode,
+    AccessCode, AccessCodeBatch, AccessCodeBatchContainer)
+
+# Public API of this submodule
+__all__ = [
+    'AccessCodeBatchContainer',
+    'AccessCodeBatch',
+    'AccessCode',
+    'get_access_code',
+    'invalidate_accesscode',
+    'disable_accesscode',
+    'reenable_accesscode',
+    'create_accesscode',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/accesscode.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/accesscode.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/accesscode.py	(revision 16045)
@@ -0,0 +1,661 @@
+## $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
+##
+"""Components to handle access codes.
+
+Access codes (aka PINs) in waeup sites are organized in batches. That
+means a certain accesscode must be part of a batch. As a site (or
+university) can hold an arbitrary number of batches, we also provide a
+batch container. Each university has one batch container that holds
+all access code batches of which each one can hold several thousands
+of access codes.
+"""
+import unicodecsv as csv # XXX: csv ops should move to dedicated module.
+import grok
+import os
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from random import SystemRandom as random
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable, IObjectHistory
+from waeup.kofa.utils.helpers import now
+from waeup.kofa.utils.logger import Logger
+from waeup.kofa.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
+    )
+from waeup.kofa.accesscodes.workflow import DISABLED, USED, ac_states_dict
+
+class AccessCode(grok.Model):
+    """An access code (aka PIN).
+
+    Implements
+    :class:`waeup.kofa.accesscodes.interfaces.IAccessCode`. :class:`AccessCode`
+    instances are normally part of an :class:`AccessCodeBatch` so
+    their representation (or code) is built with the containing batch
+    involved.
+
+    `batch_serial`
+       the serial number of the new :class:`AccessCode` inside its batch.
+
+    `random_num`
+       a 10-digit number representing the main part of the code.
+
+    :class:`AccessCode` instances normally have a representation (or
+    code) like
+
+      ``APP-XXX-YYYYYYYYYY``
+
+    where ``APP`` is the prefix of the containing batch, ``XXX`` is
+    the batch number and ``YYYYYYYYYY`` is the real code. The complete
+    PIN is portal-wide unique.
+
+    Access code instances are far more than simple strings. They have
+    a state, a history (so that all changes can be tracked) and a
+    cost (given as a float number).
+
+    The state of an access code is something like 'used', 'disabled',
+    etc. and determined by the workflow defined in
+    :mod:`waeup.kofa.accesscodes.workflow`. This also means that
+    instead of setting the status of an access code directly (you
+    can't do that easily, and yes, that's intentionally), you have to
+    trigger a transition (that might fail, if the transition is not
+    allowed in terms of logic or permissions). See
+    :mod:`waeup.kofa.accesscodes.workflow` for details.
+
+    """
+    grok.implements(IAccessCode)
+
+    def __init__(self, batch_serial=None, random_num=None):
+        super(AccessCode, self).__init__()
+        self.batch_serial = batch_serial
+        self.random_num = random_num
+        self.owner = None
+        self.cost = None
+        IWorkflowInfo(self).fireTransition('init')
+
+    @property
+    def representation(self):
+        """A string representation of the :class:`AccessCode`.
+
+        It has format ``APP-XXX-YYYYYYYYYY`` as described above.
+        """
+        return '%s-%s-%s' % (
+            self.batch_prefix, self.batch_num, self.random_num)
+
+    @property
+    def batch(self):
+        """The batch this :class:`AccessCode` is contained.
+        """
+        return getattr(self, '__parent__', None)
+
+    @property
+    def batch_prefix(self):
+        """The prefix of the batch this :class:`AccessCode` belongs to.
+        """
+        if self.batch is None:
+            return ''
+        return self.batch.prefix
+
+    @property
+    def batch_num(self):
+        """The number of the batch this :class:`AccessCode` belongs to. A
+        read-only attribute.
+        """
+        if self.batch is None:
+            return ''
+        return self.batch.num
+
+    #@property
+    #def cost(self):
+    #    """A float representing the price or ``None``. A read-only attribute.
+    #    """
+    #    if self.batch is None:
+    #        return None
+    #    return self.batch.cost
+
+    @property
+    def state(self):
+        """The workflow state. A read-only attribute.
+        """
+        return IWorkflowState(self).getState()
+
+    @property
+    def translated_state(self):
+        """The translated workflow state. A read-only attribute.
+        """
+        return ac_states_dict[self.state]
+
+    @property
+    def history(self):
+        """A :class:`waeup.kofa.objecthistory.ObjectHistory` instance.
+        """
+        history = IObjectHistory(self)
+        return '||'.join(history.messages)
+
+class AccessCodeBatch(grok.Container):
+    """A batch of access codes.
+    """
+    grok.implements(IAccessCodeBatch)
+
+    def __init__(self, creation_date=None, creator=None, batch_prefix=None,
+                 cost=None, entry_num=0, num=None):
+        super(AccessCodeBatch, self).__init__()
+        self.creation_date = creation_date
+        self.creator = creator
+        try:
+            self.prefix = batch_prefix.upper()
+        except AttributeError:
+            self.prefix = None
+        self.cost = cost
+        self.entry_num = entry_num
+        self.num = num
+        self.used_num = 0
+        self.disabled_num = 0
+
+    def _createEntries(self):
+        """Create the entries for this batch.
+        """
+        for num, pin in enumerate(self.getNewRandomNum(num=self.entry_num)):
+            self.addAccessCode(num, pin, self.cost)
+        return
+
+    def getNewRandomNum(self, num=1):
+        """Create a set of ``num`` random numbers of 10 digits each.
+
+        The number is returned as string.
+        """
+        curr = 1
+        while curr <= num:
+            pin = ''
+            for x in range(10):
+                pin += str(random().randint(0, 9))
+            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
+                curr += 1
+                yield pin
+            # PIN already in use
+
+    def _getStoragePath(self):
+        """Get the directory, where we store all batch-related CSV files.
+        """
+        site = grok.getSite()
+        storagepath = site['datacenter'].storage
+        ac_storage = os.path.join(storagepath, 'accesscodes')
+        if not os.path.exists(ac_storage):
+            os.mkdir(ac_storage)
+        return ac_storage
+
+    def getAccessCode(self, ac_id):
+        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
+        """
+        return self[ac_id]
+
+    def addAccessCode(self, num, pin, cost=0.0, owner=None):
+        """Add an access-code.
+        """
+        ac = AccessCode(num, pin)
+        if owner:
+            ac.owner = owner
+        ac.cost = cost
+        ac.__parent__ = self
+        self[ac.representation] = ac
+        return
+
+    def createCSVLogFile(self):
+        """Create a CSV file with data in batch.
+
+        Data will not contain invalidation date nor student ids.  File
+        will be created in ``accesscodes`` subdir of data center
+        storage path.
+
+        Returns name of created file.
+        """
+        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
+        ac_storage = self._getStoragePath()
+        csv_path = os.path.join(
+            ac_storage, '%s-%s-%s-%s.csv' % (
+                self.prefix, self.num, date, self.creator)
+            )
+        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
+        writer.writerow(['serial', 'ac', 'cost'])
+        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
+
+        for value in sorted(self.values(),
+                            cmp=lambda x, y: cmp(
+                x.batch_serial, y.batch_serial)):
+            writer.writerow(
+                [str(value.batch_serial), str(value.representation)]
+                )
+        site = grok.getSite()
+        logger = site.logger
+        logger.info(
+            "Created batch %s-%s" % (self.prefix, self.num))
+        logger.info(
+            "Written batch CSV to %s" % csv_path)
+        return os.path.basename(csv_path)
+
+    def archive(self):
+        """Create a CSV file for archive.
+        """
+        ac_storage = self._getStoragePath()
+        tz = getUtility(IKofaUtils).tzinfo
+        dt_now = now(tz)
+        timestamp = dt_now.strftime('%Y_%m_%d_%H_%M_%S')
+        csv_path = os.path.join(
+            ac_storage, '%s-%s_archive-%s-%s.csv' % (
+                self.prefix, self.num, timestamp, self.creator)
+            )
+        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
+        writer.writerow(['prefix', 'serial', 'ac', 'state', 'history',
+            'cost','owner'])
+        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
+                         str(self.entry_num)])
+        for value in sorted(
+            self.values(),
+            cmp = lambda x, y: cmp(x.batch_serial, y.batch_serial)
+            ):
+            writer.writerow([
+                    self.prefix, value.batch_serial, value.representation,
+                    value.state, value.history, value.cost, value.owner
+                    ])
+        return os.path.basename(csv_path)
+
+@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
+def handle_batch_added(batch, event):
+    # A (maybe dirty?) workaround to make batch containers work
+    # without self-maintained acids: as batches should contain their
+    # set of data immediately after creation, but we cannot add
+    # subobjects as long as the batch was not added already to the
+    # ZODB, we trigger the item creation for the time after the batch
+    # was added to the ZODB.
+    batch._createEntries()
+    return
+
+
+class AccessCodeBatchContainer(grok.Container, Logger):
+    grok.implements(IAccessCodeBatchContainer)
+
+    def _getStoragePath(self):
+        """Get the directory, where batch import files are stored.
+
+        If the path does not exist yet, it is created. The path is
+        normally ``accesscodes/imports`` below the datacenter storage
+        path (see :data:`waeup.kofa.accesscodes.Datacenter.storage`).
+        """
+        site = grok.getSite()
+        storagepath = site['datacenter'].storage
+        ac_storage = os.path.join(storagepath, 'accesscodes')
+        import_path = os.path.join(ac_storage, 'imports')
+        for path in [ac_storage, import_path]:
+            if not os.path.exists(path):
+                os.mkdir(path)
+                site.logger.info('created path %s' % path)
+        return import_path
+
+    def addBatch(self, batch):
+        """Add an already created `batch`.
+        """
+        batch.num = self.getNum(batch.prefix)
+        key = "%s-%s" % (batch.prefix, batch.num)
+        self[key] = batch
+        self._p_changed = True
+        return
+
+    def addBatchByImport(self, batch, batch_id):
+        """Add an already created `batch` by import with defined id.
+
+        We want to create a batch without access codes. Since num_entry
+        access codes are automatically added by handle_batch_added when
+        the batch is added to the ZODB, we have to temporarily set entry_num
+        to zero when adding the batch persistently.
+        """
+        orig_entry_num = batch.entry_num
+        batch.entry_num = 0
+        self[batch_id] = batch
+        self._p_changed = True
+        batch.entry_num = orig_entry_num
+        return
+
+    def createBatch(self, creation_date, creator, prefix, cost,
+                    entry_num):
+        """Create and add a batch.
+        """
+        batch_num = self.getNum(prefix)
+        batch = AccessCodeBatch(creation_date, creator, prefix,
+                                cost, entry_num, num=batch_num)
+        self.addBatch(batch)
+        return batch
+
+    def getNum(self, prefix):
+        """Get next unused num for given prefix.
+        """
+        # School fee, clearance, hostel application and transcript 
+        # batches start with 0.These batches are being emptily 
+        # created during initialization of the university instance.
+        if prefix in ('CLR', 'SFE', 'HOS', 'TSC'):
+            num = 0
+        else:
+            num = 1
+        while self.get('%s-%s' % (prefix, num), None) is not None:
+            num += 1
+        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
+
+    # This is temporary reimport solution. Access codes will be imported
+    # with state initialized no matter if they have been used before.
+    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['serial'])
+        num = int(entry['ac'])
+        prefix = entry['prefix']
+        batch_name = '%s-%s' % (prefix, num)
+        if batch_name in self.keys():
+            raise KeyError('Batch already exists: %s' % batch_name)
+        batch = AccessCodeBatch(
+            datetime.utcnow(), creator, prefix, cost, 0, num=num)
+        num_entries = 0
+        self[batch_name] = batch
+        for row in reader:
+            pin = row['ac']
+            serial = int(row['serial'])
+            try:
+                cost = float(row['cost'])
+            except ValueError:
+                cost = 0.0
+            rand_num = pin.rsplit('-', 1)[-1]
+            batch.addAccessCode(serial, rand_num, cost)
+            num_entries += 1
+        batch.entry_num = num_entries
+        batch.createCSVLogFile()
+        return
+
+    def getAccessCode(self, ac_id):
+        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
+        """
+        for batchname in self.keys():
+            batch = self[batchname]
+            try:
+                return batch.getAccessCode(ac_id)
+            except KeyError:
+                continue
+        return None
+
+    def disable(self, ac_id, comment=None):
+        """Disable the AC with ID ``ac_id``.
+
+        ``user_id`` is the user ID of the user triggering the
+        process. Already disabled ACs are left untouched.
+        """
+        ac = self.getAccessCode(ac_id)
+        if ac is None:
+            return
+        disable_accesscode(ac_id, comment)
+        return
+
+    def enable(self, ac_id, comment=None):
+        """(Re-)enable the AC with ID ``ac_id``.
+
+        This leaves the given AC in state ``unused``. Already enabled
+        ACs are left untouched.
+        """
+        ac = self.getAccessCode(ac_id)
+        if ac is None:
+            return
+        reenable_accesscode(ac_id, comment)
+        return
+
+    logger_name = 'waeup.kofa.${sitename}.accesscodes'
+    logger_filename = 'accesscodes.log'
+
+    def logger_info(self, ob_class, comment=None):
+        """Get the logger's info method.
+        """
+        self.logger.info('%s - %s' % (
+                ob_class, comment))
+        return
+
+class AccessCodePlugin(grok.GlobalUtility):
+    grok.name('accesscodes')
+    grok.implements(IKofaPluggable)
+
+    def setup(self, site, name, logger):
+        basecontainer = AccessCodeBatchContainer()
+        site['accesscodes'] = basecontainer
+        logger.info('Installed container for access code batches.')
+        # Create empty school fee, clearance, hostel application
+        # and transcript AC
+        # batches during initialization of university instance.
+        cost = 0.0
+        creator = 'system'
+        entry_num = 0
+        creation_date = datetime.utcnow()
+        basecontainer.createBatch(creation_date, creator,
+            'SFE', cost, entry_num)
+        basecontainer.createBatch(creation_date, creator,
+            'CLR', cost, entry_num)
+        basecontainer.createBatch(creation_date, creator,
+            'HOS', cost, entry_num)
+        basecontainer.createBatch(creation_date, creator,
+            'TSC', cost, entry_num)
+        logger.info('Installed empty SFE, CLR, HOS and TSC access code batches.')
+        return
+
+    def update(self, site, name, logger):
+        site_name = getattr(site, '__name__', '<Unnamed Site>')
+        if not 'accesscodes' in site.keys():
+            logger.info('Updating site at %s. Installing access codes.' % (
+                    site,))
+            self.setup(site, name, logger)
+        else:
+            logger.info(
+                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
+                    site_name, ))
+        return
+
+class AccessCodeBatchFactory(grok.GlobalUtility):
+    """A factory for accesscodebatches.
+
+    We need this factory for the accesscodebatchprocessor.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.AccessCodeBatch')
+    title = u"Create a new accesscode batch.",
+    description = u"This factory instantiates new accesscode batch instances."
+
+    def __call__(self, *args, **kw):
+        return AccessCodeBatch()
+
+    def getInterfaces(self):
+        return implementedBy(AccessCodeBatch)
+
+class AccessCodeFactory(grok.GlobalUtility):
+    """A factory for accesscodes.
+
+    We need this factory for the accesscodeprocessor.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.AccessCode')
+    title = u"Create a new accesscode.",
+    description = u"This factory instantiates new accesscode instances."
+
+    def __call__(self, *args, **kw):
+        return AccessCode(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(AccessCode)
+
+def get_access_code(access_code):
+    """Get an access code instance.
+
+    An access code here is a string like ``PUDE-1-1234567890``.
+
+    Returns ``None`` if the given code cannot be found.
+
+    This is a convenicence function that is faster than looking up a
+    batch container for the approriate access code.
+    """
+    site = grok.getSite()
+    if not isinstance(access_code, basestring):
+        return None
+    try:
+        batch_id, ac_id = access_code.rsplit('-', 1)
+    except:
+        return None
+    batch = site['accesscodes'].get(batch_id, None)
+    if batch is None:
+        return None
+    try:
+        code = batch.getAccessCode(access_code)
+    except KeyError:
+        return None
+    return code
+
+def fire_transition(access_code, arg, toward=False, comment=None, owner=None):
+    """Fire workflow transition for access code.
+
+    The access code instance is looked up via `access_code` (a string
+    like ``APP-1-12345678``).
+
+    `arg` tells what kind of transition to trigger. This will be a
+    transition id like ``'use'`` or ``'init'``, or some transition
+    target like :data:`waeup.kofa.accesscodes.workflow.INITIALIZED`.
+
+    If `toward` is ``False`` (the default) you have to pass a
+    transition id as `arg`, otherwise you must give a transition
+    target.
+
+    If `comment` is specified (default is ``None``) the given string
+    will be passed along as transition comment. It will appear in the
+    history of the changed access code. You can use this to place
+    remarks like for which object the access code was used or similar.
+
+    If `owner` is specified, the owner attribute of the access code is checked.
+    If the access code has an owner but is different, :func:`fire_transition`
+    fails and returns False.
+
+    :func:`fire_transition` might raise exceptions depending on the
+    reason why the requested transition cannot be performed.
+
+    The following exceptions can occur during processing:
+
+    :exc:`KeyError`:
+      signals not existent access code, batch or site.
+
+    :exc:`ValueError`:
+      signals illegal format of `access_code` string. The regular format is
+      ``APP-N-XXXXXXXX``.
+
+    :exc:`hurry.workflow.interfaces.InvalidTransitionError`:
+      the transition requested cannot be performed because the workflow
+      rules forbid it.
+
+    :exc:`Unauthorized`:
+      the current user is not allowed to perform the requested transition.
+
+    """
+    try:
+        batch_id, ac_id = access_code.rsplit('-', 1)
+    except ValueError:
+        raise ValueError(
+            'Invalid access code format: %s (use: APP-N-XXXXXXXX)' % (
+                access_code,))
+    try:
+        ac = grok.getSite()['accesscodes'][batch_id].getAccessCode(access_code)
+    except TypeError:
+        raise KeyError(
+            'No site available for looking up accesscodes')
+    if owner:
+        ac_owner = getattr(ac, 'owner', None)
+        if ac_owner and ac_owner != owner:
+            return False
+    info = IWorkflowInfo(ac)
+    if toward:
+        info.fireTransitionToward(arg, comment=comment)
+    else:
+        info.fireTransition(arg, comment=comment)
+    return True
+
+def invalidate_accesscode(access_code, comment=None, owner=None):
+    """Invalidate AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    try:
+        return fire_transition(access_code, 'use', comment=comment, owner=owner)
+    except:
+        return False
+
+def disable_accesscode(access_code, comment=None):
+    """Disable AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(
+        access_code, DISABLED, toward=True, comment=comment)
+
+def reenable_accesscode(access_code, comment=None):
+    """Reenable AccessCode denoted by string ``access_code``.
+
+    Fires an appropriate transition to perform the task.
+
+    `comment` is a string that will appear in the access code
+    history.
+
+    See :func:`fire_transition` for possible exceptions and their
+    meanings.
+    """
+    return fire_transition(access_code, 'reenable', comment=comment)
+
+def create_accesscode(batch_prefix, batch_num, cost, owner):
+    """
+    """
+    batch_id = '%s-%s' % (batch_prefix, batch_num)
+    try:
+        batch = grok.getSite()['accesscodes'][batch_id]
+    except KeyError:
+        return None, u'No AC batch available.'
+    rand_num = list(batch.getNewRandomNum())[0]
+    num = len(batch) + 1
+    batch.addAccessCode(num, rand_num, cost, owner)
+    batch.entry_num += 1
+    pin = u'%s-%s-%s' % (batch_prefix,batch_num,rand_num)
+    return pin, None
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/batching.py	(revision 16045)
@@ -0,0 +1,231 @@
+## $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
+##
+"""Batch processing components for accesscodes.
+
+"""
+import grok
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from zope.interface import Interface
+from waeup.kofa.interfaces import (
+    IBatchProcessor, IGNORE_MARKER, IObjectHistory, IObjectConverter)
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch, IAccessCode
+from waeup.kofa.accesscodes.workflow import INITIALIZED, USED, DISABLED
+
+IMPORTABLE_TRANSITIONS = (
+    'init', 'use', 'disable_used', 'disable_unused', 'reeanble')
+
+IMPORTABLE_STATES = (INITIALIZED, USED, DISABLED)
+
+class AccessCodeBatchProcessor(BatchProcessor):
+    """The Access Code Batch Processor imports containers for access codes.
+    It does not import their content. There is nothing special about this
+    processor.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'accesscodebatchprocessor'
+    grok.name(util_name)
+
+    name = u'AccessCodeBatch Processor'
+    iface = IAccessCodeBatch
+
+    location_fields = ['batch_id',]
+    factory_name = 'waeup.AccessCodeBatch'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'accesscodes' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['batch_id'] in site['accesscodes'].keys()
+
+    def getParent(self, row, site):
+        return site['accesscodes']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['batch_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addBatchByImport(obj, row['batch_id'])
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+
+        Returns a string describing the fields changed.
+        """
+        items_changed = super(AccessCodeBatchProcessor, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite()['accesscodes'].logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
+
+class AccessCodeProcessor(BatchProcessor):
+    """The Access Code Processor imports access codes (ac) into their
+    batches. Localization requires the `representation` of the ac
+    (object id) as well as  `batch_prefix` and `batch_num` to find
+    the parent container (the batch).
+
+    Access codes follow a workflow. The `checkConversion` method validates
+    the workflow `state` and `transition` in row. `checkUpdateRequirements`
+    checks if the transition is allowed. This depends on the context.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'accesscodeprocessor'
+    grok.name(util_name)
+
+    name = u'AccessCode Processor'
+    iface = IAccessCode
+
+    location_fields = ['representation', 'batch_prefix', 'batch_num']
+    factory_name = 'waeup.AccessCode'
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.location_fields + [
+                        'random_num', 'owner', 'cost',
+                        'state', 'transition', 'batch_serial',]
+                    )))
+
+    @property
+    def required_fields(self):
+        return ['random_num', 'batch_serial', 'history',]
+
+    def parentsExist(self, row, site):
+        return self.getParent(row,site) is not None
+
+    def entryExists(self, row, site):
+        parent = self.getParent(row, site)
+        if parent is None:
+            return False
+        return row['representation'] in parent.keys()
+
+    def getParent(self, row, site):
+        if not 'accesscodes' in site.keys():
+            return None
+        batch_id = '%s-%s' % (row['batch_prefix'], row['batch_num'])
+        return site['accesscodes'].get(batch_id, None)
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['representation'], None)
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent[row['representation']] = obj
+        return
+
+    def checkConversion(self, row, mode='create'):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        if 'transition' in row and 'state' in row:
+            if row['transition'] not in (IGNORE_MARKER, '') and \
+                row['state'] not in (IGNORE_MARKER, ''):
+                errs.append(('workflow','not allowed'))
+                return errs, inv_errs, conv_dict
+        if 'transition' in row:
+            if row['transition'] not in IMPORTABLE_TRANSITIONS:
+                if row['transition'] not in (IGNORE_MARKER, ''):
+                    errs.append(('transition','not allowed'))
+        if 'state' in row:
+            if row['state'] not in IMPORTABLE_STATES:
+                if row['state'] not in (IGNORE_MARKER, ''):
+                    errs.append(('state','not allowed'))
+                else:
+                    # State is an attribute of AccessCode and must not
+                    # be changed if empty.
+                    conv_dict['state'] = IGNORE_MARKER
+        return errs, inv_errs, conv_dict
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        transition = row.get('transition', IGNORE_MARKER)
+        if transition not in (IGNORE_MARKER, ''):
+            allowed_transitions = IWorkflowInfo(obj).getManualTransitionIds()
+            if transition not in allowed_transitions:
+                return 'Transition not allowed.'
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+
+        Returns a string describing the fields changed.
+        """
+        items_changed = ''
+        # Update state
+        # Attention: When importing states the counters remain unchanged.
+        if 'state' in row:
+            state = row.get('state', IGNORE_MARKER)
+            if state not in (IGNORE_MARKER, ''):
+                value = row['state']
+                IWorkflowState(obj).setState(value)
+                items_changed += ('%s=%s, ' % ('state', state))
+                msg = "state '%s' set" % state
+                IObjectHistory(obj).addMessage(msg)
+            row.pop('state')
+        # Trigger transition. Counters are properly changed.
+        if 'transition' in row:
+            transition = row.get('transition', IGNORE_MARKER)
+            if transition not in (IGNORE_MARKER, ''):
+                value = row['transition']
+                IWorkflowInfo(obj).fireTransition(value)
+                items_changed += ('%s=%s, ' % ('transition', transition))
+            row.pop('transition')
+
+        # In import files we can use the hash symbol at the end of a
+        # random_num string to avoid annoying automatic number transformation
+        # by Excel or Calc
+        if 'random_num' in row:
+            random_num = row.get('random_num', IGNORE_MARKER)
+            if random_num not in (IGNORE_MARKER, ''):
+                row['random_num'] = random_num.strip('#')
+
+        items_changed += super(AccessCodeProcessor, self).updateEntry(
+            obj, row, site, filename)
+
+        # Log actions...
+        grok.getSite()['accesscodes'].logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row['representation'], items_changed))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser.py	(revision 16045)
@@ -0,0 +1,266 @@
+## $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 accesscodes.
+"""
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from hurry.workflow.interfaces import InvalidTransitionError
+from waeup.kofa.browser.layout import KofaPage, KofaAddFormPage, NullValidator
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.viewlets import (
+    AdminTask, AddActionButton, SearchActionButton, BatchOpButton, ManageLink)
+from waeup.kofa.interfaces import IKofaObject, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.accesscodes.interfaces import (
+    IAccessCodeBatchContainer, IAccessCodeBatch,
+    )
+from waeup.kofa.accesscodes.catalog import search
+from waeup.kofa.browser.layout import action
+
+grok.context(IKofaObject)
+
+class BatchContainerPage(KofaPage):
+    grok.name('index')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('batchcontainer')
+    grok.require('waeup.manageACBatches')
+    archive_button = _('Archive')
+    delete_button = _('Archive and delete')
+
+    label = _('Access Code Batches')
+    pnav = 0
+
+    def update(self, batches=None, archive=None, delete=None):
+        if archive is None and delete is None:
+            return
+        if not batches:
+            self.flash(_('No batch selected.'), type='warning')
+            return
+        if isinstance(batches, basestring):
+            batches = [batches]
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        for name in batches:
+            batch = self.context[name]
+            csv_file = batch.archive()
+            self.flash(_('Archived ${a} (${b})',
+                mapping = {'a':name, 'b':csv_file}))
+            message = 'archived: %s (%s)' % (name,csv_file)
+            self.context.logger_info(ob_class, message)
+            if delete is None:
+                continue
+            del self.context[name]
+            self.context._p_changed = True
+            self.flash(_('Deleted batch ${a}', mapping = {'a':name}))
+            message = 'deleted: %s' % name
+            self.context.logger_info(ob_class, message)
+
+class AddBatchPage(KofaAddFormPage):
+    grok.name('add')
+    grok.context(IAccessCodeBatchContainer)
+    grok.require('waeup.manageACBatches')
+
+    label = _('Create Access Code Batch')
+    pnav = 0
+
+    form_fields = grok.AutoFields(IAccessCodeBatch).select(
+        'prefix', 'entry_num', 'cost')
+
+    @action(_('Create batch'), style='primary')
+    def createBatch(self, **data):
+        creator = self.request.principal.id
+        creation_date = datetime.utcnow()
+        data.update(creation_date=creation_date, creator=creator)
+        batch = self.context.createBatch(**data)
+        csv_file = batch.createCSVLogFile()
+        self.context._p_changed = True
+        self.flash(_('Batch created (${a} entries)',
+            mapping = {'a':data['entry_num']}))
+        self.flash(_('Data written to ${a}', mapping = {'a':csv_file}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        message = 'created: %s (%d, %f)' % (
+            csv_file, data['entry_num'], data['cost'])
+        self.context.logger_info(ob_class, message)
+        self.redirect(self.url(self.context))
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, *args, **kw):
+        self.flash(_('Batch creation cancelled.'), type='warning')
+        self.redirect(self.url(self.context))
+
+class ReimportBatchPage(KofaPage):
+    """Screen for reimporting AC batches.
+    """
+    grok.name('reimport')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('reimportbatchpage')
+    grok.require('waeup.manageACBatches')
+    reimport_button = _('Reimport')
+    cancel_button = _('Cancel')
+
+    label = _('Reimport Access Code Batches')
+    pnav = 0
+
+    def update(self, filenames=None, reimport=None, cancel=None):
+        if cancel is not None:
+            self.flash(_('Reimport cancelled.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        if reimport is None:
+            return
+        if not filenames:
+            self.flash(_('No file chosen. Action cancelled.'), type='warning')
+            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: ${a}',
+                    mapping = {'a':filename}), type='warning')
+                continue
+            self.flash(_('Successfully reimported: ${a}',
+                mapping = {'a':filename}))
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            message = 'reimported: %s' % filename
+            self.context.logger_info(ob_class, message)
+        self.redirect(self.url(self.context))
+
+class BatchContainerSearchPage(KofaPage):
+    grok.name('search')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('searchpage')
+    grok.require('waeup.manageACBatches')
+    pnav = 0
+    label = _('Search and Manage Access Codes')
+    search_button = _('Search')
+    disable_button = _('Disable ACs')
+    enable_button = _('Enable ACs')
+    cancel_button = _('Cancel Search')
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        if 'cancel' in form:
+            self.redirect(self.url(self.context))
+            return
+        self.hitlist = []
+        if 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            return
+        if not 'entries' in form:
+            self.hitlist = search(query=self.searchterm,
+                searchtype=self.searchtype, view=self)
+            return
+        entries = form['entries']
+        if isinstance(entries, basestring):
+            entries = [entries]
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        for entry in entries:
+            if 'disable' in form:
+                try:
+                    comment = _(u"disabled")
+                    self.context.disable(entry, comment)
+                    self.flash(_('${a} disabled.', mapping = {'a':entry}))
+                    message = 'disabled: %s' % entry
+                    self.context.logger_info(ob_class, message)
+                except InvalidTransitionError:
+                    self.flash(_('${a}: Disable transition not allowed.',
+                        mapping = {'a':entry}), type='danger')
+            elif 'enable' in form:
+                try:
+                    comment = _(u"re-enabled")
+                    self.context.enable(entry, comment)
+                    self.flash(_('${a} (re-)enabled.', mapping = {'a':entry}))
+                    message = '(re-)enabled: %s' % entry
+                    self.context.logger_info(ob_class, message)
+                except InvalidTransitionError:
+                    self.flash(_('${a}: Re-enable transition not allowed.',
+                               mapping = {'a':entry}), type='danger')
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        return
+
+class BatchContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for ac batch containers.
+    """
+    grok.require('waeup.manageACBatches')
+    grok.context(IAccessCodeBatchContainer)
+    title = _(u'Access Code Batches')
+    parent_viewname = 'administration'
+
+#class BatchContainerSearchBreadcrumb(Breadcrumb):
+#    """A breadcrumb for ac batch containers search page.
+#    """
+#    grok.require('waeup.manageACBatches')
+#    grok.context(IAccessCodeBatchContainer)
+#    grok.name('search')
+#    title = u'Search Access Codes'
+#    viewname = 'search'
+#    parent_viewname = 'index'
+
+class AdminTaskManageACBatches(AdminTask):
+    """Entry on administration page that links to batch container.
+    """
+    grok.order(5)
+    grok.require('waeup.manageACBatches')
+    grok.template('admintaskacbatches')
+
+    link_title = _('Access Code Batches')
+    target_viewname = 'accesscodes'
+
+class CreateBatchButton(AddActionButton):
+    """Action button on batch container page which links to batch creation.
+    """
+    grok.context(IAccessCodeBatchContainer)
+    grok.view(BatchContainerPage)
+    grok.require('waeup.manageACBatches')
+    text = _('Add Access Code Batch')
+
+class ReimportBatchButton(BatchOpButton):
+    """Action button on batch container page which links to batch reimport.
+    """
+    grok.context(IAccessCodeBatchContainer)
+    grok.view(BatchContainerPage)
+    grok.require('waeup.manageACBatches')
+    target = 'reimport'
+    text = _('Reimport Access Code Batch')
+
+class SearchAccessCodeButton(SearchActionButton):
+    """Action button on batch container page which links to search.
+    """
+    grok.context(IAccessCodeBatchContainer)
+    grok.view(BatchContainerPage)
+    grok.require('waeup.manageACBatches')
+    text = _('Search Access Codes')
+
+class ManageAccessCodes(ManageLink):
+    """Link in upper left box to access code management.
+    """
+    grok.order(5)
+    grok.require('waeup.manageACBatches')
+
+    link = u'accesscodes'
+    text = _(u'Access Codes')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<p>
+<a href=""
+   tal:attributes="href viewlet/link_target"
+   tal:content="viewlet/link_title">MANAGE</a>
+</p>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt	(revision 16045)
@@ -0,0 +1,54 @@
+<form i18n:domain="waeup.kofa" method="POST">
+  <p i18n:translate="">
+    The following batches are available:
+  </p>
+  <table class="table">
+    <thead>
+      <tr>
+        <th>&nbsp;</th>
+        <th i18n:translate="">Prefix</th>
+        <th i18n:translate="">Entries</th>
+        <th i18n:translate="">Used</th>
+        <th i18n:translate="">Disabled</th>
+        <th i18n:translate="">Cost</th>
+        <th i18n:translate="">Date of Creation</th>
+        <th i18n:translate="">Creator</th>
+      </tr>
+    </thead>
+    <tbody>
+  <tr tal:repeat="batch context/values">
+  <td>
+    <input type="checkbox" name="batches" value="batch/prefix"
+     tal:attributes="value batch/__name__"
+     tal:condition="batch/num" />
+  </td>
+  <td>
+    <span tal:replace="batch/prefix">APP</span>
+    -
+    <span tal:replace="batch/num">10</span>
+  </td>
+  <td>
+    <span tal:replace="batch/entry_num">1012</span>
+  </td>
+  <td>
+    <span tal:replace="batch/used_num">512</span>
+  </td>
+  <td>
+    <span tal:replace="batch/disabled_num">112</span>
+  </td>
+  <td tal:content="batch/cost">12.12</td>
+  <td tal:content="python: batch.creation_date.strftime('%Y-%m-%d %H:%M:%S')">
+      2011-11-22 18:04:55
+  </td>
+  <td tal:content="batch/creator">SOMEUSER</td>
+      </tr>
+      <tr tal:condition="not: context/values">
+      <td colspan="5"><b i18n:translate="">No batches yet</b></td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" class="btn btn-default" name="archive"
+      tal:attributes="value view/archive_button" />
+  <input type="submit" class="btn btn-default" name="delete"
+       tal:attributes="value view/delete_button" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt	(revision 16045)
@@ -0,0 +1,31 @@
+<p i18n:domain="waeup.kofa">
+Please select the files to reimport. Please note, that batches with
+same name have to be deleted before.
+</p>
+<form method="POST" i18n:domain="waeup.kofa">
+  <table class="table">
+    <thead>
+      <tr>
+        <th>&nbsp;
+        </th>
+        <th i18n:translate="">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">FILENAME</td>
+      </tr>
+      <tr tal:condition="python: len(list(context.getImportFiles())) == 0">
+        <td colspan="5"><b i18n:translate="">No import batches available</b></td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" class="btn btn-primary" name="reimport"
+      tal:attributes="value view/reimport_button" />
+  <input type="submit" class="btn btn-default" name="cancel"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt	(revision 16045)
@@ -0,0 +1,63 @@
+<form method="POST" i18n:domain="waeup.kofa" class="form-inline">
+<br />
+  <div class="form-group">
+    <input type="submit" class="btn btn-primary" name="search"
+        tal:attributes="value view/search_button" />
+  <span i18n:translate="">for access codes</span>
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="code">with PIN</option>
+      <option i18n:translate="" value="batch_serial">with serial</option>
+      <option i18n:translate="" value="history">with message term</option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+  <p>&nbsp;</p>
+  <div tal:condition="view/hitlist">
+    <h3>Search Results</h3>
+    <input type="hidden" name="old_searchterm"
+         tal:attributes="value view/searchterm" />
+    <input type="hidden" name="old_searchtype"
+         tal:attributes="value view/searchtype" />
+    <table class="kofa-data-table dataTableManage">
+      <thead>
+      <tr>
+        <th>&nbsp;</th>
+        <th width="50px" i18n:translate="">Serial</th>
+        <th i18n:translate="">AC</th>
+        <th i18n:translate="">State</th>
+        <th i18n:translate="">Owner</th>
+        <th i18n:translate="">History</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr tal:repeat="item view/hitlist">
+        <td><input type="checkbox" name="entries"
+                   tal:attributes="value item/code" /></td>
+        <td tal:content="item/batch_serial">1</td>
+        <td tal:content="item/code">APP-1-1234567890</td>
+        <td tal:content="item/translated_state">unused</td>
+        <td tal:content="item/owner">Id</td>
+          <td tal:content="structure python:item.history.replace('||','<br />')">
+            history
+          </td>
+      </tr>
+      </tbody>
+    </table>
+    <div>
+      <input type="submit" class="btn btn-default" name="disable"
+          tal:attributes="value view/disable_button" />
+      <input type="submit" class="btn btn-default" name="enable"
+          tal:attributes="value view/enable_button" />
+      <input type="submit" class="btn btn-default" name="cancel"
+          tal:attributes="value view/cancel_button" />
+    </div>
+  </div>
+  <div tal:condition="not: view/hitlist">
+    <input type="submit" class="btn btn-default" name="cancel"
+        tal:attributes="value view/cancel_button" />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/catalog.py	(revision 16045)
@@ -0,0 +1,74 @@
+## $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
+##
+"""Cataloging and searching components for access codes.
+"""
+import grok
+from grok import index
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope.index.text.parsetree import ParseError
+from waeup.kofa.interfaces import IUniversity, IQueryResultItem
+from waeup.kofa.accesscodes.interfaces import IAccessCode
+
+class AccessCodeIndexes(grok.Indexes):
+    """A catalog for access codes.
+    """
+    grok.site(IUniversity)
+    grok.name('accesscodes_catalog')
+    grok.context(IAccessCode)
+
+    code = index.Field(attribute='representation')
+    history = index.Text(attribute='history')
+    batch_serial = index.Field(attribute='batch_serial')
+    state = index.Field(attribute='state')
+
+class AccessCodeQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    title = u'Access Code Query Item'
+    description = u'Some access code found in a search'
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.code = context.representation
+        self.history = context.history
+        self.translated_state = context.translated_state
+        self.owner = context.owner
+        self.batch_serial = context.batch_serial
+
+def search(query=None, searchtype=None, view=None):
+    if not query:
+        view.flash('Empty search string.')
+        return
+    hitlist = []
+    if searchtype == 'history':
+        results = Query().searchResults(
+            Text(('accesscodes_catalog', searchtype), query))
+    elif searchtype == 'batch_serial':
+        try:
+            results = Query().searchResults(
+                Eq(('accesscodes_catalog', searchtype), int(query)))
+        except ValueError:
+            return
+    else:
+        results = Query().searchResults(
+            Eq(('accesscodes_catalog', searchtype), query))
+    for result in results:
+        hitlist.append(AccessCodeQueryResultItem(result, view=view))
+    return hitlist
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/export.py	(revision 16045)
@@ -0,0 +1,90 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for access codes and access code batches.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names
+from waeup.kofa.accesscodes.interfaces import IAccessCode, IAccessCodeBatch
+
+class AccessCodeBatchExporter(grok.GlobalUtility, ExporterBase):
+    """The Access Code Batch Exporter exports container data. It does not
+    export access codes inside the container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('accesscodebatches')
+
+    fields = tuple(sorted(iface_names(IAccessCodeBatch))) + ('batch_id',)
+    title = _(u'Access Code Batches')
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'batch_id' and context is not None:
+            value = u'%s-%s' % (getattr(context, 'prefix', None),
+                getattr(context, 'num', None))
+        return super(
+            AccessCodeBatchExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export_all(self, site, filepath=None):
+        """Export accesscode batches into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        accesscodebatches = site.get('accesscodes', {})
+        for batch in accesscodebatches.values():
+            self.write_item(batch, writer)
+        return self.close_outfile(filepath, outfile)
+
+
+class AccessCodeExporter(grok.GlobalUtility, ExporterBase):
+    """The Access Code Exporter exports all access codes stored in the
+    access code batch containers. The exporter iterates over all access code
+    batches and over all access codes inside each batch container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('accesscodes')
+
+    fields = tuple(sorted(iface_names(IAccessCode)))
+    title = _(u'Access Codes')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler adds a hash symbol at the end of ``random_num``
+        to avoid annoying automatic number transformation by Excel or Calc.
+        """
+        if name == 'random_num' and value is not None:
+            # Append hash '#' to numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            AccessCodeExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export_all(self, site, filepath=None):
+        """Export accesscodes into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        accesscodebatches = site.get('accesscodes', {})
+        for batch in accesscodebatches.values():
+            for ac in batch.values():
+                self.write_item(ac, writer)
+        return self.close_outfile(filepath, outfile)
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/interfaces.py	(revision 16045)
@@ -0,0 +1,102 @@
+## $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
+##
+"""Interfaces of access code related components.
+"""
+from zope import schema
+from zope.interface import Interface
+from waeup.kofa.interfaces import IKofaObject
+from waeup.kofa.interfaces import MessageFactory as _
+
+class IAccessCode(IKofaObject):
+    """An access code.
+    """
+    batch_serial = schema.Int(
+        title = _(u'Serial number inside batch'),
+        )
+    batch_prefix = schema.TextLine(
+        title = _(u'Prefix inside batch'),
+        )
+    batch_num = schema.Int(
+        title = _(u'Batch number'),
+        )
+    random_num = schema.TextLine(
+        title = _(u'Random part of access code'),
+        )
+    cost = schema.Float(
+        title = _(u'Cost of access code'),
+        )
+    state = schema.TextLine(
+        title = _(u'Workflow state'),
+        required = False,
+        )
+    representation = schema.TextLine(
+        title = _(u'Complete title of access code'),
+        )
+    owner = schema.TextLine(
+        title = _(u'Purchaser'),
+        required = False,
+        )
+    history = schema.Text(
+        title = _(u'The history of access code as lines'),
+        default = u'',
+        readonly = True,
+        required = False,
+       )
+
+class IAccessCodeBatch(Interface):
+    """A factory for batches of access codes.
+    """
+    creation_date = schema.Datetime(
+        title = _(u'Creation date'),
+        )
+    creator = schema.TextLine(
+        title = _(u'Batch creator'),
+        )
+    prefix = schema.TextLine(
+        title = _(u'Batch prefix'),
+        )
+    num = schema.Int(
+        title = _(u'Batch number (1-3 digits)'),
+        min = 0, max = 999,
+        )
+    entry_num = schema.Int(
+        title = _(u'Number of access codes'),
+        default = 1000, min = 0,
+        )
+    cost = schema.Float(
+        title = _(u'Cost of access code'),
+        default = 0.0, min = 0.0,
+        )
+    disabled_num = schema.Int(
+        title = _(u'Number of disabled access codes inside the batch'),
+        default = 0,
+        readonly = True,
+        )
+    used_num = schema.Int(
+        title = _(u'Number of used access codes inside the batch'),
+        default = 0,
+        readonly = True,
+        )
+
+class IAccessCodeBatchContainer(IKofaObject):
+    """A container for access code batches.
+    """
+
+    def addBatch(batch):
+        """Add a batch.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 16045)
@@ -0,0 +1,9 @@
+batch_num,batch_prefix,batch_serial,cost,history,owner,random_num,representation,state,transition
+1,CLR,33,100.0,anything,K1000009,1625368961#,CLR-1-1625368961,used,
+1,CLR,2,100.0,anything,,5468453598#,CLR-1-5468453598,initialized,
+1,HOS,456,0.0,anything,,98769876#,HOS-1-98769876,,use
+1,HOS,456,0.0,anything,,76254765#,HOS-1-76254765,,
+1,HOS,23,0.0,anything,,0007044547#,HOS-1-0007044547,nonsense,
+1,HOS,731,0.0,anything,,555#,HOS-1-555,used,disable_unused
+1,HOS,456,0.0,anything,,666#,HOS-1-666,,nonsense
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv	(revision 16045)
@@ -0,0 +1,3 @@
+batch_num,batch_prefix,representation,state,transition
+1,CLR,CLR-1-1625368961,disabled,
+1,HOS,HOS-1-98769876,,disable_used
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv	(revision 16045)
@@ -0,0 +1,5 @@
+batch_id,cost,creation_date,creator,disabled_num,entry_num,num,prefix,used_num
+CLR-1,0.0,2012-04-28 07:28:48.719026,system,0,3,1,CLR,1
+HOS-2,0.0,2012-04-28 07:28:48.719026,system,0,0,2,HOS,0
+HOS-1,0.0,2012-09-19 08:59:27.341100,zope.manager,0,1000,1,HOS,1
+SFE-3,0.0,2012-04-28 07:28:48.719026,system,0,8,3,SFE,0
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_import.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_import.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/sample_import.csv	(revision 16045)
@@ -0,0 +1,5 @@
+"prefix","serial","ac","state","history","cost"
+"FOO","9.99","1","0",""
+"FOO","0","FOO-1-11111111","used","2011-06-01 12:12:14 - AC initialized by system||2011-06-01 12:14:14 - comment with ""quotes"" by system","1000.0"
+"FOO","1","FOO-1-22222222","initialized","2011-05-30 21:10:12 - AC initialized by system",""
+"FOO","2","FOO-1-33333333","disabled","2012-05-30 21:12:10 - AC initialized by system||2012-05-30 23:10:10 - AC disabled by system",""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_accesscode.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_accesscode.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_accesscode.py	(revision 16045)
@@ -0,0 +1,475 @@
+## $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
+##
+import doctest
+import os
+import re
+import shutil
+import tempfile
+import unittest
+
+from datetime import datetime
+from hurry.workflow.interfaces import InvalidTransitionError, IWorkflowState
+from zope.component import getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyObject, verifyClass
+from zope.testing import renormalizing
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IObjectHistory, IKofaPluggable, IKofaUtils
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, setUp, tearDown, getRootFolder)
+from waeup.kofa.accesscodes.accesscode import (
+    AccessCodeBatch, get_access_code, invalidate_accesscode, AccessCode,
+    disable_accesscode, reenable_accesscode, fire_transition,
+    AccessCodeBatchContainer, AccessCodePlugin)
+from waeup.kofa.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch,  IAccessCodeBatchContainer,)
+from waeup.kofa.accesscodes.workflow import INITIALIZED, USED, DISABLED
+
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+class AccessCodeHelpersTests(FunctionalTestCase):
+    # Tests for helpers like get_access_code, disable_accesscode, ...
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeHelpersTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        # Create batch
+        batch = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        # Fill batch with accesscodes
+        batch.addAccessCode(0, '11111111')
+        batch.addAccessCode(1, '22222222')
+        batch.addAccessCode(2, '33333333')
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        self.ac2 = batch.getAccessCode('APP-1-22222222')
+        self.ac3 = batch.getAccessCode('APP-1-33333333')
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeHelpersTests, self).tearDown()
+        return
+
+    def test_get_access_code(self):
+        ac = get_access_code('APP-1-11111111')
+        assert ac is self.ac1
+
+    def test_get_access_code_not_string(self):
+        ac = get_access_code(object())
+        assert ac is None
+
+    def test_get_access_code_no_proper_pin(self):
+        ac = get_access_code('APP-without_pin')
+        assert ac is None
+
+    def test_get_access_code_invalid_batch_num(self):
+        ac = get_access_code('APP-invalid-11111111')
+        assert ac is None
+
+    def test_get_access_code_invalid_pin(self):
+        ac = get_access_code('APP-1-notexistent')
+        assert ac is None
+
+    def test_invalidate_accesscode(self):
+        assert self.ac1.state != USED
+        result = invalidate_accesscode('APP-1-11111111')
+        assert self.ac1.state == USED
+        assert result is True
+
+    def test_disable_accesscode_unused(self):
+        # we can disable initialized acs
+        assert self.ac1.state != USED
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1.state == DISABLED
+
+    def test_disable_accesscode_used(self):
+        # we can disable already used acs
+        assert self.ac1.state != DISABLED
+        invalidate_accesscode('APP-1-11111111')
+        disable_accesscode('APP-1-11111111')
+        assert self.ac1.state == DISABLED
+
+    def test_reenable_accesscode(self):
+        # we can reenable disabled acs
+        disable_accesscode('APP-1-11111111')
+        result = reenable_accesscode('APP-1-11111111')
+        assert result is True
+        assert self.ac1.state != USED
+
+    def test_fire_transition(self):
+        # we can fire transitions generally
+        fire_transition('APP-1-11111111', 'use')
+        assert IWorkflowState(self.ac1).getState() is USED
+
+    def test_fire_transition_toward(self):
+        # the `toward` keyword is respected
+        fire_transition('APP-1-11111111', DISABLED, toward=True)
+        assert IWorkflowState(self.ac1).getState() is DISABLED
+
+    def test_fire_transition_no_site(self):
+        # when no site is available, we will get a TypeError
+        clearSite()
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-11111111', 'use')
+
+    def test_fire_transition_broken_ac_id(self):
+        # if we get an invalid access code id (of wrong format) we get
+        # ValueErrors
+        self.assertRaises(
+            ValueError,
+            fire_transition, '11111111', 'use')
+
+    def test_fire_transition_invalid_batch_id(self):
+        # if we request a non-existent batch_id, we'll get a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'FOO-1-11111111', 'use')
+
+    def test_fire_transition_invalid_ac(self):
+        # if we request a non-exitent access-code, we'll get a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-NONSENSE', 'use')
+
+    def test_fire_transition_undef_trans_id(self):
+        # asking for undefined transition id means a KeyError
+        self.assertRaises(
+            KeyError,
+            fire_transition, 'APP-1-11111111', 'nonsense')
+
+    def test_fire_transition_invalid_transition(self):
+        # asking for a forbidden transition will result in
+        # InvalidTransitionError
+        self.assertRaises(
+            InvalidTransitionError,
+            fire_transition, 'APP-1-11111111', 'init') # already initialized
+
+    def test_fire_transition_comment(self):
+        # when we request a comment, it will also appear in history
+        fire_transition('APP-1-11111111', 'use', comment='Hi there!')
+        history = IObjectHistory(self.ac1)
+        msgs = history.messages
+        assert 'Hi there!' in msgs[-1]
+
+    def test_fire_transition_no_comment(self):
+        # without comment, the history should be without trailing garbage
+        fire_transition('APP-1-11111111', 'use')
+        history = IObjectHistory(self.ac1)
+        msgs = history.messages
+        assert msgs[-1].endswith('used by system')
+
+class AccessCodeTests(FunctionalTestCase):
+    # Tests for AccessCode class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        # Create batch
+        batch = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        # Fill batch with accesscodes
+        batch.addAccessCode(0, '11111111')
+
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        # AccessCodes fullfill their iface promises.
+        ac = AccessCode('1', '12345678')
+        assert verifyObject(IAccessCode, ac)
+        assert verifyClass(IAccessCode, AccessCode)
+
+    def test_history(self):
+        # Access codes have a history.
+        match = re.match(
+            '^....-..-.. ..:..:.. .+ - initialized by system',
+            self.ac1.history)
+        assert match is not None
+
+    def test_cost(self):
+        # The cost of an access code will be stored by handle_batch_added
+        # right after the batch has been added to the ZODB. Thus after
+        # creation of the batch, cost is still 0.0
+        cost = self.ac1.cost
+        assert cost == 0.0
+
+class AccessCodeBatchTests(FunctionalTestCase):
+    # Tests for AccessCodeBatch class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeBatchTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        batch = AccessCodeBatch(    # create batch with zero entries
+            datetime.utcnow(), 'testuser', 'FOO', 9.99, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        self.ac1 = AccessCode(0, '11111111')
+        self.ac1.cost = 2345.0
+        self.ac2 = AccessCode(1, '22222222')
+        self.ac3 = AccessCode(2, '33333333')
+        batch['FOO-1-11111111'] = self.ac1
+        batch['FOO-1-22222222'] = self.ac2
+        batch['FOO-1-33333333'] = self.ac3
+        self.batch = batch
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeBatchTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        batch = AccessCodeBatch(
+            datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
+        assert verifyObject(IAccessCodeBatch, batch)
+        assert verifyClass(IAccessCodeBatch, AccessCodeBatch)
+
+    def test_container_contents(self):
+        assert 'SFE-0' in self.app['accesscodes']
+        assert 'HOS-0' in self.app['accesscodes']
+        assert 'CLR-0' in self.app['accesscodes']
+
+    def test_csv_export(self):
+        # Make sure CSV export of accesscodes works
+        batch = self.batch
+        invalidate_accesscode('FOO-1-11111111', comment='comment with "quotes"')
+        disable_accesscode('FOO-1-33333333')
+        basename = batch.archive()
+        result_path = os.path.join(batch._getStoragePath(), basename)
+        expected = '''
+"prefix","serial","ac","state","history","cost","owner"
+"FOO","9.99","1","0"
+"FOO","0","FOO-1-11111111","used","<YYYY-MM-DD hh:mm:ss> UTC - ...","2345.0",""
+"FOO","1","FOO-1-22222222","initialized","<YYYY-MM-DD hh:mm:ss> UTC - ...",""
+"FOO","2","FOO-1-33333333","disabled","<YYYY-MM-DD hh:mm:ss> UTC - ...",""
+'''[1:]
+        contents = open(result_path, 'rb').read()
+        self.assertMatches(expected, contents)
+
+class AccessCodeBatchContainerTests(FunctionalTestCase):
+    # Tests for AccessCodeContainer class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodeBatchContainerTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.import_sample1_src = os.path.join(
+            os.path.dirname(__file__), 'sample_import.csv')
+
+        batch = AccessCodeBatch(    # create batch with zero entries
+            datetime.now(), 'testuser', 'BAR', 9.99, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        self.ac1 = AccessCode(0, '11111111')
+        self.ac2 = AccessCode(1, '22222222')
+        self.ac3 = AccessCode(2, '33333333')
+        batch['BAR-1-11111111'] = self.ac1
+        batch['BAR-1-22222222'] = self.ac2
+        batch['BAR-1-33333333'] = self.ac3
+        self.batch = batch
+
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodeBatchContainerTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        accesscodes = AccessCodeBatchContainer()
+        assert verifyObject(IAccessCodeBatchContainer, accesscodes)
+        assert verifyClass(IAccessCodeBatchContainer, AccessCodeBatchContainer)
+
+    def test_existing_batches(self):
+        self.assertEqual(sorted(self.app['accesscodes'].keys()),
+            [u'BAR-1', u'CLR-0', u'HOS-0', u'SFE-0', u'TSC-0'])
+
+    def test_csv_import(self):
+        # Make sure we can reimport sample data from local sample_import.csv
+        batchcontainer = self.app['accesscodes']
+        shutil.copyfile(        # Copy sample to import dir
+            os.path.join(os.path.dirname(__file__), 'sample_import.csv'),
+            os.path.join(batchcontainer._getStoragePath(), 'sample_import.csv')
+            )
+        batchcontainer.reimport('sample_import.csv')
+        batch = batchcontainer.get(u'FOO-1', None)
+        self.assertTrue(batch is not None)
+        keys = [x for x in batch.keys()]
+        self.assertEqual(
+            keys,
+            [u'FOO-1-11111111', u'FOO-1-22222222', u'FOO-1-33333333'])
+        # Also cost has been stored correctly
+        self.assertEqual(batch['FOO-1-11111111'].cost,1000.0)
+
+    def test_getAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        result1 = batchcontainer.getAccessCode('BAR-1-11111111')
+        result2 = batchcontainer.getAccessCode('BAR-1-not-existent')
+        assert isinstance(result1, AccessCode)
+        assert result2 is None
+
+    def test_disableAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        result1 = batchcontainer.disable('BAR-1-11111111')
+        result2 = batchcontainer.disable('BAR-1-not-existent')
+        assert self.ac1.state is DISABLED
+        assert result2 is None
+
+    def test_enableAccessCode(self):
+        batchcontainer = self.app['accesscodes']
+        batchcontainer.disable('BAR-1-11111111')
+        result1 = batchcontainer.enable('BAR-1-11111111')
+        result2 = batchcontainer.enable('BAR-1-not-existent')
+        assert self.ac1.state is INITIALIZED
+        assert result2 is None
+
+class AccessCodePluginTests(FunctionalTestCase):
+    # Tests for AccessCodeContainer class
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccessCodePluginTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(AccessCodePluginTests, self).tearDown()
+        return
+
+    def test_iface(self):
+        plugin = AccessCodePlugin()
+        assert verifyObject(IKofaPluggable, plugin)
+        assert verifyClass(IKofaPluggable, AccessCodePlugin)
+
+    def test_update_w_ac_container(self):
+        # The plugin changes nothing, if there is already a container
+        plugin = AccessCodePlugin()
+        site = self.app
+        logger = site.logger
+        accesscodes = site['accesscodes']
+        plugin.update(site, 'app', logger)
+        assert site['accesscodes'] is accesscodes
+
+    def test_update_wo_ac_container(self):
+        # The plugin creates a new accesscodes container if it is missing
+        plugin = AccessCodePlugin()
+        site = self.app
+        logger = site.logger
+        del site['accesscodes']
+        plugin.update(site, 'app', logger)
+        assert 'accesscodes' in site
+
+checker = renormalizing.RENormalizing([
+        (re.compile('[\d]{10}'), '<10-DIGITS>'),
+        ])
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        AccessCodeHelpersTests,
+        AccessCodeTests,
+        AccessCodeBatchTests,
+        AccessCodeBatchContainerTests,
+        AccessCodePluginTests,
+        ]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
+    for filename in [
+        '../doctests/accesscode_browser.txt'
+        ]:
+        path = os.path.join(
+            os.path.dirname(os.path.dirname(__file__)), filename)
+        test = doctest.DocFileSuite(
+            path,
+            module_relative=False,
+            setUp=setUp, tearDown=tearDown,
+            globs = dict(getRootFolder = getRootFolder),
+            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
+            checker = checker,
+            )
+        test.layer = FunctionalLayer
+        suite.addTest(test)
+    return suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,226 @@
+## $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
+##
+"""
+Tests for hostels and their UI components.
+"""
+import os
+import shutil
+import tempfile
+import grok
+import pytz
+from datetime import datetime, timedelta
+from zope.event import notify
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component.hooks import setSite, clearSite
+from zope.testbrowser.testing import Browser
+from zope.security.interfaces import Unauthorized
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IObjectHistory, IKofaPluggable, IKofaUtils
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, setUp, tearDown, getRootFolder)
+from waeup.kofa.accesscodes.batching import (
+    AccessCodeBatchProcessor, AccessCodeProcessor)
+
+
+BATCH_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_batch_data.csv'),
+    'rb').read()
+
+BATCH_HEADER_FIELDS = BATCH_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+AC_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_ac_data.csv'),
+    'rb').read()
+
+AC_HEADER_FIELDS = AC_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+AC_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_ac_data_update.csv'),
+    'rb').read()
+
+AC_HEADER_FIELDS_UPDATE = AC_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class ACFullSetup(FunctionalTestCase):
+
+    def setUp(self):
+        super(ACFullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'accesscodes.log')
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        super(ACFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+        shutil.rmtree(self.workdir)
+
+class ACBatchProcessorTest(ACFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_import(self):
+        self.processor = AccessCodeBatchProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_batch_data.csv')
+        open(self.csv_file, 'wb').write(BATCH_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, BATCH_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        self.assertEqual(len(self.app['accesscodes'].keys()), 8)
+        self.assertEqual(self.app['accesscodes']['CLR-1'].num,1)
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'system - AccessCodeBatch Processor - sample_batch_data - '
+            'CLR-1 - updated: '
+            'num=1, creator=system, used_num=1, '
+            'entry_num=3, creation_date=2012-04-28 07:28:48.719026+00:00, '
+            'prefix=CLR, cost=0.0, disabled_num=0'
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+class ACProcessorTest(ACFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_import_create(self):
+        self.processor = AccessCodeBatchProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_batch_data.csv')
+        open(self.csv_file, 'wb').write(BATCH_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, BATCH_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = AccessCodeProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_ac_data.csv')
+        open(self.csv_file, 'wb').write(AC_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, AC_HEADER_FIELDS)
+        self.assertEqual(num_warns,3)
+        fail_file = open(fail_file).read()
+        self.assertTrue(
+            'HOS-1-0007044547,HOS,anything,state: not allowed' in fail_file)
+        self.assertTrue(
+            'HOS-1-555,HOS,anything,workflow: not allowed' in fail_file)
+        self.assertTrue(
+            'HOS-1-666,HOS,anything,transition: not allowed' in fail_file)
+        self.assertEqual(
+            self.app['accesscodes']['HOS-1']['HOS-1-98769876'].state, 'used')
+        self.assertEqual(
+            self.app['accesscodes']['HOS-1']['HOS-1-76254765'].state,
+            'initialized')
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].batch_serial,
+            33)
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].random_num,
+            '1625368961')
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].state, 'used')
+        self.assertMatches(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].history,
+            "<YYYY-MM-DD hh:mm:ss> UTC - initialized by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - state 'used' set by system")
+        self.assertMatches(
+            self.app['accesscodes']['HOS-1']['HOS-1-98769876'].history,
+            "<YYYY-MM-DD hh:mm:ss> UTC - initialized by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - used by system")
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - AccessCode Processor - sample_ac_data - '
+            'CLR-1-1625368961 - updated: state=used, '
+            'batch_serial=33, random_num=1625368961, '
+            'cost=100.0, owner=K1000009'
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+    def test_import_update(self):
+        self.processor = AccessCodeBatchProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_batch_data.csv')
+        open(self.csv_file, 'wb').write(BATCH_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, BATCH_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = AccessCodeProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_ac_data.csv')
+        open(self.csv_file, 'wb').write(AC_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, AC_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.assertEqual(self.app['accesscodes']['HOS-1'].used_num, 1)
+        self.assertEqual(self.app['accesscodes']['HOS-1'].disabled_num, 0)
+        self.assertEqual(self.app['accesscodes']['CLR-1'].used_num, 1)
+        self.assertEqual(self.app['accesscodes']['CLR-1'].disabled_num, 0)
+
+        self.csv_file = os.path.join(self.workdir, 'sample_ac_data_update.csv')
+        open(self.csv_file, 'wb').write(AC_SAMPLE_DATA_UPDATE)
+        num, num_warns, fin_file, fail_file =  self.processor.doImport(
+            self.csv_file, AC_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        self.assertEqual(
+            self.app['accesscodes']['HOS-1']['HOS-1-98769876'].state,
+            'disabled')
+        self.assertEqual(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].state,
+            'disabled')
+        self.assertMatches(
+            self.app['accesscodes']['CLR-1']['CLR-1-1625368961'].history,
+            "<YYYY-MM-DD hh:mm:ss> UTC - initialized by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - state 'used' set by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - state 'disabled' set by system")
+        self.assertMatches(
+            self.app['accesscodes']['HOS-1']['HOS-1-98769876'].history,
+            "<YYYY-MM-DD hh:mm:ss> UTC - initialized by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - used by system|"
+            "|<YYYY-MM-DD hh:mm:ss> UTC - disabled by system")
+        # When importing transitions the counters are properly set.
+        self.assertEqual(self.app['accesscodes']['HOS-1'].used_num, 0)
+        self.assertEqual(self.app['accesscodes']['HOS-1'].disabled_num, 1)
+        # When importing states the counters remain unchanged.
+        self.assertEqual(self.app['accesscodes']['CLR-1'].used_num, 1)
+        self.assertEqual(self.app['accesscodes']['CLR-1'].disabled_num, 0)
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_catalog.py	(revision 16045)
@@ -0,0 +1,227 @@
+## $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
+##
+import shutil
+import tempfile
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IQueryResultItem
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.accesscodes.accesscode import (
+    AccessCodeBatch, invalidate_accesscode, disable_accesscode)
+from waeup.kofa.accesscodes.workflow import INITIALIZED, USED, DISABLED
+from waeup.kofa.accesscodes.catalog import AccessCodeQueryResultItem, search
+
+class CatalogTestSetup(FunctionalTestCase):
+    # A setup for testing accesscode catalog related stuff.
+    #
+    # sets up a site with some accesscode batches already created.
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CatalogTestSetup, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+
+        # Create batch
+        batch = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        # Fill batch with accesscodes
+        batch.addAccessCode(0, '11111111')
+        batch.addAccessCode(1, '22222222')
+        batch.addAccessCode(2, '33333333')
+        self.ac1 = batch.getAccessCode('APP-1-11111111')
+        self.ac2 = batch.getAccessCode('APP-1-22222222')
+        self.ac3 = batch.getAccessCode('APP-1-33333333')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(CatalogTestSetup, self).tearDown()
+        return
+
+
+class AccessCodeCatalogTests(CatalogTestSetup):
+    # Tests for helpers like get_access_code, disable_accesscode, ...
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an accesscodes catalog if we wish
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        assert cat is not None
+
+    def test_search_by_code(self):
+        # We can find a certain code
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        results = cat.searchResults(code=('APP-1-11111111', 'APP-1-11111111'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.ac1
+
+    def test_search_code_not_existent(self):
+        # Not existent codes will not be found
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        results = cat.searchResults(code=('APP-1-blah', 'APP-1-blah'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 0
+
+    def test_search_history(self):
+        # We can search for certain history entries
+        # To update history we use `invalidate_accesscode`
+        invalidate_accesscode(
+            'APP-1-11111111', comment='used by Tester')
+
+        # Now we want to find the term ``Tester`` in histories of all acs
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        results = cat.searchResults(history='Tester')
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.ac1
+
+    def test_search_disabled(self):
+        # We can seach for disabled access codes
+        disable_accesscode('APP-1-11111111')
+        # Now we want to find the disabled access code
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        results1 = cat.searchResults(state=(DISABLED, DISABLED))
+        results2 = cat.searchResults(state=(INITIALIZED, INITIALIZED))
+        results1 = [x for x in results1] # Turn results generator into list
+        # We found 1 accescode disabled
+        assert len(results1) == 1
+        assert results1[0] is self.ac1
+        # We found 2 accesscodes not disabled
+        assert len(results2) == 2
+        assert [x for x in results2] == [self.ac2, self.ac3]
+
+    def test_search_used(self):
+        # We can search for used/unused access codes
+        invalidate_accesscode('APP-1-11111111')
+        # Now we want to find the invalidated access code
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        results1 = cat.searchResults(state=(USED, USED))
+        results2 = cat.searchResults(state=(INITIALIZED, INITIALIZED))
+        results1 = [x for x in results1] # Turn results generator into list
+        # We found 1 accescode used
+        assert len(results1) == 1
+        assert results1[0] is self.ac1
+        # We found 2 accesscodes not used
+        assert len(results2) == 2
+        assert [x for x in results2] == [self.ac2, self.ac3]
+
+    def test_search_mixed(self):
+        # We can ask for several attributes at the same time
+        invalidate_accesscode('APP-1-11111111', comment='Used by Tester')
+        disable_accesscode('APP-1-33333333', comment='Disabled by Tester')
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        # Now we search for an applicants code that has 'Tester'
+        # mentioned in history, is used, and has a code between
+        # 'APP-1-11111111' and 'APP-2-11111111'. This should find
+        # exactly one access code.
+        results = cat.searchResults(history='Tester',
+                                    state=(USED, USED),
+                                    code=('APP-1-11111111', 'APP-1-22222222'))
+        assert len(results) == 1
+        assert self.ac1 in results
+
+    def test_ac_change(self):
+        # When an AC is changed, that change will be reflected
+        # immediately in catalog
+        cat = queryUtility(ICatalog, name='accesscodes_catalog')
+        result = cat.searchResults(state=(USED, USED))
+        assert len(result) == 0
+        invalidate_accesscode('APP-1-11111111')
+        result = cat.searchResults(state=(USED, USED))
+        assert len(result) == 1
+
+class FakeView(object):
+    # A view we can use in tests. Provides only the neccessary methods.
+    flashed = []
+    def url(self, context):
+        pass
+    def flash(self, msg):
+        self.flashed.append(msg)
+
+class AccessCodeQueryResultItemTests(CatalogTestSetup):
+    # Test query result items
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # Make sure we implement the interfaces correctly
+        view = FakeView()
+        self.assertTrue(verifyClass(
+                IQueryResultItem, AccessCodeQueryResultItem))
+        item = AccessCodeQueryResultItem(self.ac1, view)
+        self.assertTrue(verifyObject(
+                IQueryResultItem, item))
+        return
+
+class SearchTests(CatalogTestSetup):
+    # Tests for the search() function
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SearchTests, self).setUp()
+        self.view = FakeView()
+        return
+
+    def test_search_none(self):
+        # if we search none we will get it. Also a warning is displayed.
+        result = search(view=self.view)
+        self.assertTrue(result is None)
+        self.assertEqual(self.view.flashed, ['Empty search string.'])
+        return
+
+    def test_search_history(self):
+        # we can search for history entries
+        result = search(
+            query='initialized', # A word that appears in all items' hist
+            searchtype='history', view=self.view)
+        self.assertEqual(len(result), 3)
+        return
+
+    def test_search_batch_serial(self):
+        # we can search for batch serials
+        result = search(
+            query=2,
+            searchtype='batch_serial', view=self.view)
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].code, 'APP-1-33333333')
+        return
+
+    def test_search_freestyle(self):
+        # we can even search for arbitrary fields
+        result = search(
+            query='APP-1-11111111',
+            searchtype='code', view=self.view)
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].code, 'APP-1-11111111')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_export.py	(revision 16045)
@@ -0,0 +1,76 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component import queryUtility
+from zope.interface.verify import verifyObject, verifyClass
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.testing import KofaUnitTestLayer, FunctionalLayer
+from waeup.kofa.accesscodes.accesscode import (
+    AccessCodeBatchContainer, AccessCodeBatch)
+from waeup.kofa.accesscodes.export import AccessCodeBatchExporter
+
+
+class AccessCodeBatchExporterTest(unittest.TestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = AccessCodeBatchExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, AccessCodeBatchExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a faculty exporter as utility
+        result = queryUtility(ICSVExporter, name="accesscodebatches")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_all(self):
+        # we can export all faculties in a site
+        container = AccessCodeBatchContainer()
+        site = {'accesscodes':container}
+        batch1 = AccessCodeBatch('now', 'manfred', 'APP', 6.6, 0)
+        batch2 = AccessCodeBatch('now', 'manfred', 'SFE', 6.6, 0)
+        container.addBatch(batch1)
+        container.addBatch(batch2)
+        exporter = AccessCodeBatchExporter()
+        exporter.export_all(site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+             'cost,creation_date,creator,disabled_num,entry_num,num,'
+             'prefix,used_num,batch_id\r\n6.6,now,manfred,0,0,1,APP,0,APP-1\r\n'
+             '6.6,now,manfred,0,0,0,SFE,0,SFE-0\r\n'
+            )
+        return
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/tests/test_workflow.py	(revision 16045)
@@ -0,0 +1,47 @@
+## $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
+##
+import shutil
+import tempfile
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.accesscodes.workflow import (
+    invalidate_action, disable_used_action, disable_unused_action,
+    reenable_action,
+    )
+
+class WorkflowActionsTests(FunctionalTestCase):
+    # Tests for helpers like get_access_code, disable_accesscode, ...
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(WorkflowActionsTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        super(WorkflowActionsTests, self).tearDown()
+        return
+
+    def test_actions_wo_parents(self):
+        # We can pass contexts without parent to actions
+        context = object() # Some fake context without parent
+        self.assertTrue(invalidate_action(None, context) is None)
+        self.assertTrue(disable_used_action(None, context) is None)
+        self.assertTrue(disable_unused_action(None, context) is None)
+        self.assertTrue(reenable_action(None, context) is None)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/accesscodes/workflow.py	(revision 16045)
@@ -0,0 +1,141 @@
+## $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
+##
+"""
+Workflows for access codes/pins.
+"""
+import grok
+from zope.i18n import translate
+from zope.component import getUtility
+from datetime import datetime
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.kofa.accesscodes.interfaces import IAccessCode
+from waeup.kofa.interfaces import IObjectHistory, IKofaWorkflowInfo
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
+
+INITIALIZED = 'initialized'
+USED = 'used'
+DISABLED = 'disabled'
+
+ac_states_dict = {
+    INITIALIZED: _('initialized'),
+    USED: _('used'),
+    DISABLED: _('disabled'),
+    }
+
+def invalidate_action(wf, context):
+    """Side actions taken when an access code is invalidated.
+    """
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    batch.used_num += 1
+    return
+
+def disable_used_action(wf, context):
+    """Side actions taken when a used access code is disabled.
+    """
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    batch.used_num -= 1
+    batch.disabled_num += 1
+
+def disable_unused_action(wf, context):
+    """Side actions taken when an unused access code is invalidated.
+    """
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    batch.disabled_num += 1
+    return
+
+def reenable_action(wf, context):
+    """Side actions taken when an access code is reenabled.
+    """
+    batch = getattr(context, '__parent__', None)
+    if batch is None:
+        return
+    batch.disabled_num -= 1
+    return
+
+ACCESSCODE_TRANSITIONS = (
+    Transition(
+        transition_id = 'init',
+        title = _('Initialize PIN'),
+        source = None,
+        condition = NullCondition,
+        destination = INITIALIZED),
+
+    Transition(
+        transition_id = 'use',
+        title = _('Use PIN'),
+        source = INITIALIZED,
+        destination = USED,
+        action = invalidate_action),
+
+    Transition(
+        transition_id = 'disable_unused',
+        title = _('Disable unused PIN'),
+        source = INITIALIZED,
+        destination = DISABLED,
+        action = disable_unused_action),
+
+    Transition(
+        transition_id = 'disable_used',
+        title = _('Disable used PIN'),
+        source = USED,
+        destination = DISABLED,
+        action = disable_used_action),
+
+    Transition(
+        transition_id = 'reenable',
+        title = _('Reenable disabled PIN'),
+        source = DISABLED,
+        destination = INITIALIZED,
+        action = reenable_action),
+    )
+
+accesscode_workflow = KofaWorkflow(ACCESSCODE_TRANSITIONS)
+
+class AccessCodeWorkflowState(WorkflowState, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.accesscode.state'
+    state_id = 'wf.accesscode.id'
+
+class AccessCodeWorkflowInfo(KofaWorkflowInfo, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(IKofaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = accesscode_workflow
+
+@grok.subscribe(IAccessCode, IWorkflowTransitionEvent)
+def handle_accesscode_transition_event(obj, event):
+    # append message to history
+    if event.comment is not None:
+        msg = event.comment
+    else:
+        msg = ac_states_dict[event.destination]
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/app.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/app.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/app.py	(revision 16045)
@@ -0,0 +1,109 @@
+## $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
+##
+import grok
+from zope.authentication.interfaces import IAuthentication
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.interfaces import ObjectEvent
+from zope.pluggableauth import PluggableAuthentication
+from waeup.kofa.authentication import setup_authentication
+from waeup.kofa.datacenter import DataCenter
+from waeup.kofa.mandates.container import MandatesContainer
+from waeup.kofa.interfaces import (
+    IUniversity, IKofaPluggable, IObjectUpgradeEvent, IJobManager,
+    VIRT_JOBS_CONTAINER_NAME)
+from waeup.kofa.userscontainer import UsersContainer
+from waeup.kofa.utils.logger import Logger
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.configuration import ConfigurationContainer
+
+
+class University(grok.Application, grok.Container, Logger):
+    """A university.
+    """
+    grok.implements(IUniversity)
+
+    # Setup authentication for this app. Note: this is only
+    # initialized, when a new instance of this app is created.
+    grok.local_utility(
+        PluggableAuthentication, provides=IAuthentication,
+        setup=setup_authentication,)
+
+    def __init__(self, *args, **kw):
+        super(University, self).__init__(*args, **kw)
+        self.setup()
+        return
+
+    def setup(self):
+        """Setup some hard-wired components.
+
+        Create local datacenter, containers for users, students and
+        the like.
+        """
+        from waeup.kofa.students.container import StudentsContainer
+        from waeup.kofa.hostels.container import HostelsContainer
+
+        self['users'] = UsersContainer()
+        self['datacenter'] = DataCenter()
+        self['students'] = StudentsContainer()
+        self['configuration'] = ConfigurationContainer()
+        self['hostels'] = HostelsContainer()
+        self['mandates'] = MandatesContainer()
+        self._createPlugins()
+
+    def _createPlugins(self):
+        """Create instances of all plugins defined somewhere.
+        """
+        for name, plugin in getUtilitiesFor(IKofaPluggable):
+            plugin.setup(self, name, self.logger)
+        return
+
+    def traverse(self, name):
+        if name == VIRT_JOBS_CONTAINER_NAME:
+            return getUtility(IJobManager)
+        return None
+
+    def updatePlugins(self):
+        """Lookup an arbitrarily selected set of plugins and call their
+        `update()` method to upgrade Kofa's database since software version 1.4
+        (2016-01-14).
+
+        XXX: This method does not run all plugins registered.
+
+        XXX: Tests for this method were disabled.
+        """
+        for name in ['faculties', 'departments', 'certificates',
+                     'certcourses', 'courses', 'hostels',
+                     'site-pluggable-auth']:
+            getUtility(IKofaPluggable, name=name).update(
+                self, name, self.logger)
+        return
+attrs_to_fields(University)
+
+
+class ObjectUpgradeEvent(ObjectEvent):
+    """An event fired, when datacenter storage moves.
+    """
+    grok.implements(IObjectUpgradeEvent)
+
+
+@grok.subscribe(University, grok.IObjectAddedEvent)
+def handle_university_added(app, event):
+    """If a university is added, a message is logged.
+    """
+    app.logger.info('University `%s` added.' % getattr(app, '__name__', None))
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/__init__.py	(revision 16045)
@@ -0,0 +1,21 @@
+"""This package contains everything regarding university applicants.
+"""
+# Make this a package.
+from waeup.kofa.applicants.applicant import (
+    Applicant, ApplicantFactory, ApplicantImageStoreHandler,
+    ApplicantImageNameChooser,
+    )
+from waeup.kofa.applicants.container import ApplicantsContainer
+from waeup.kofa.applicants.root import ApplicantsRoot
+from waeup.kofa.applicants.dynamicroles import (
+    ApplicantPrincipalRoleManager,)
+
+__all__ = [
+    'Applicant',
+    'ApplicantFactory',
+    'ApplicantImageNameChooser',
+    'ApplicantImageStoreHandler',
+    'ApplicantPrincipalRoleManager',
+    'ApplicantsContainer',
+    'ApplicantsRoot',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/applicant.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/applicant.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/applicant.py	(revision 16045)
@@ -0,0 +1,465 @@
+## $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
+##
+import os
+import grok
+from cStringIO import StringIO
+from grok import index
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope.component import getUtility, createObject, getAdapter
+from zope.component.interfaces import IFactory
+from zope.event import notify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.interface import implementedBy
+from zope.schema.interfaces import RequiredMissing, ConstraintNotSatisfied
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.kofa.image import KofaImageFile
+from reportlab.platypus.doctemplate import LayoutError
+from waeup.kofa.imagestorage import DefaultFileStoreHandler
+from waeup.kofa.interfaces import (
+    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser, IKofaUtils,
+    IExtFileStore, IPDF, IUserAccount, IUniversity)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.vocabularies import RegNumNotInSource
+from waeup.kofa.students.studycourse import StudentStudyCourse
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.applicants.interfaces import (
+    IApplicant, IApplicantEdit, ISpecialApplicant, IApplicantsUtils)
+from waeup.kofa.applicants.workflow import application_states_dict
+from waeup.kofa.applicants.payment import ApplicantOnlinePayment
+from waeup.kofa.applicants.refereereport import ApplicantRefereeReport
+
+def search(query=None, searchtype=None, view=None):
+    if searchtype in ('fullname',):
+        results = Query().searchResults(
+            Text(('applicants_catalog', searchtype), query))
+    else:
+        results = Query().searchResults(
+            Eq(('applicants_catalog', searchtype), query))
+    return results
+
+class Applicant(grok.Container):
+    grok.implements(IApplicant, IApplicantEdit, ISpecialApplicant)
+    grok.provides(IApplicant)
+
+    applicant_student_mapping = [
+        ('firstname', 'firstname'),
+        ('middlename', 'middlename'),
+        ('lastname', 'lastname'),
+        ('sex', 'sex'),
+        ('date_of_birth', 'date_of_birth'),
+        ('email', 'email'),
+        ('phone', 'phone'),
+        ]
+
+    def __init__(self):
+        super(Applicant, self).__init__()
+        self.password = None
+        self.application_date = None
+        self.applicant_id = None
+        return
+
+    @property
+    def payments(self):
+        payments = [value for value in self.values()
+            if isinstance(value, ApplicantOnlinePayment)]
+        return payments
+
+    @property
+    def refereereports(self):
+        reports = [value for value in self.values()
+            if isinstance(value, ApplicantRefereeReport)]
+        return reports
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.applicant_id, message))
+        return
+
+    @property
+    def state(self):
+        return IWorkflowState(self).getState()
+
+    @property
+    def container_code(self):
+        try:
+            code = self.__parent__.code
+        except AttributeError:  # in unit tests
+            return
+        if (self.password,
+            self.firstname,
+            self.lastname,
+            self.email) == (None, None, None, None):
+            return code + '-'
+        return code + '+'
+
+    @property
+    def translated_state(self):
+        try:
+            state = application_states_dict[self.state]
+        except LookupError:  # in unit tests
+            return
+        return state
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def special(self):
+        try:
+            special = self.__parent__.prefix.startswith('special')
+        except AttributeError:  # in unit tests
+            return
+        return special
+
+    @property
+    def application_number(self):
+        try:
+            return self.applicant_id.split('_')[1]
+        except AttributeError:
+            return None
+
+    @property
+    def display_fullname(self):
+        middlename = getattr(self, 'middlename', None)
+        kofa_utils = getUtility(IKofaUtils)
+        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
+
+    def _setStudyCourseAttributes(self, studycourse):
+        studycourse.entry_mode = self.course_admitted.study_mode
+        studycourse.current_level = self.course_admitted.start_level
+        studycourse.certificate = self.course_admitted
+        studycourse.entry_session = self.__parent__.year
+        studycourse.current_session = self.__parent__.year
+        return
+
+    def createStudent(self, view=None):
+        """Create a student, fill with base data, create an application slip,
+        copy applicant data and files.
+        """
+        site = grok.getSite()
+        # Is applicant in the correct state?
+        if self.state != 'admitted':
+            return False, _('Applicant has not yet been admitted.')
+        # Does registration number exist?
+        student = createObject(u'waeup.Student')
+        try:
+            student.reg_number = self.reg_number
+        except RegNumNotInSource:
+            # Reset _curr_stud_id 
+            site['students']._curr_stud_id -= 1
+            return False, _('Registration Number exists.')
+        # Has the course_admitted field been properly filled?
+        if self.course_admitted is None:
+            # Reset _curr_stud_id
+            site['students']._curr_stud_id -= 1
+            return False, _('No course admitted provided.')
+        # Set student attributes
+        try:
+            for item in self.applicant_student_mapping:
+                setattr(student, item[1], getattr(self, item[0], None))
+        except RequiredMissing, err:
+            site['students']._curr_stud_id -= 1
+            return False, 'RequiredMissing: %s' % err
+        except:
+            site['students']._curr_stud_id -= 1
+            return False, 'Unknown Error'
+        # Prove if the certificate still exists
+        try:
+            StudentStudyCourse().certificate = self.course_admitted
+        except ConstraintNotSatisfied, err:
+            # Reset _curr_stud_id
+            site['students']._curr_stud_id -= 1
+            return False, 'ConstraintNotSatisfied: %s' % self.course_admitted.code
+        # Finally prove if an application slip can be created
+        try:
+            test_applicant_slip = getAdapter(self, IPDF, name='application_slip')(
+                view=view)
+        except IOError:
+            site['students']._curr_stud_id -= 1
+            return False, _('IOError: Application Slip could not be created.')
+        except LayoutError, err:
+            site['students']._curr_stud_id -= 1
+            return False, _('Reportlab LayoutError: %s' % err)
+        except AttributeError, err:
+            site['students']._curr_stud_id -= 1
+            return False, _('Reportlab AttributeError: ${a}', mapping = {'a':err})
+        except:
+            site['students']._curr_stud_id -= 1
+            return False, _('Unknown Reportlab error')
+        # Add student
+        site['students'].addStudent(student)
+        # Save student_id
+        self.student_id = student.student_id
+        # Fire transitions
+        IWorkflowInfo(self).fireTransition('create')
+        IWorkflowInfo(student).fireTransition('admit')
+        # Set password
+        IUserAccount(student).setPassword(self.application_number)
+        # Save the certificate and set study course attributes
+        self._setStudyCourseAttributes(student['studycourse'])
+        self._copyPassportImage(student)
+        self._copyFiles(student)
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(student))
+        # Save application slip
+        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
+            view=view)
+        self._saveApplicationPDF(student, applicant_slip, view=view)
+        return True, _('Student ${a} created', mapping = {'a':student.student_id})
+
+    def _saveApplicationPDF(self, student, applicant_slip, view=None):
+        """Create an application slip as PDF and store it in student folder.
+        """
+        file_store = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr="application_slip.pdf")
+        file_store.createFile(file_id, StringIO(applicant_slip))
+        return
+
+    def _copyPassportImage(self, student):
+        """Copy any passport image over to student location.
+        """
+        file_store = getUtility(IExtFileStore)
+        appl_file = file_store.getFileByContext(self)
+        if appl_file is None:
+            return
+        stud_file_id = IFileStoreNameChooser(student).chooseName(
+            attr="passport.jpg")
+        file_store.createFile(stud_file_id, appl_file)
+        return
+
+    def _copyFiles(self, student):
+        """Copy all other files over to student location. Not
+        used in base package but tested with fake file.
+        """
+        file_store = getUtility(IExtFileStore)
+        filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
+        for filename in filenames:
+            appl_file = file_store.getFileByContext(self, attr=filename[1])
+            if appl_file is None:
+                continue
+            stud_file_id = IFileStoreNameChooser(student).chooseName(
+                attr=filename[1])
+            file_store.createFile(stud_file_id, appl_file)
+        return
+
+# Set all attributes of Applicant required in IApplicant as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+Applicant = attrs_to_fields(Applicant)
+
+class ApplicantsCatalog(grok.Indexes):
+    """A catalog indexing :class:`Applicant` instances in the ZODB.
+    """
+    grok.site(IUniversity)
+    grok.name('applicants_catalog')
+    grok.context(IApplicant)
+
+    fullname = index.Text(attribute='display_fullname')
+    applicant_id = index.Field(attribute='applicant_id')
+    reg_number = index.Field(attribute='reg_number')
+    email = index.Field(attribute='email')
+    state = index.Field(attribute='state')
+    container_code = index.Field(attribute='container_code')
+
+class ApplicantFactory(grok.GlobalUtility):
+    """A factory for applicants.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Applicant')
+    title = u"Create a new applicant.",
+    description = u"This factory instantiates new applicant instances."
+
+    def __call__(self, *args, **kw):
+        return Applicant()
+
+    def getInterfaces(self):
+        return implementedBy(Applicant)
+
+
+#: The file id marker for applicant passport images
+APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
+
+class ApplicantImageNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Applicant` objects.
+
+    `context` is an :class:`Applicant` instance.
+
+    The :class:`ApplicantImageNameChooser` can build/check file ids
+    for :class:`Applicant` objects suitable for use with
+    :class:`ExtFileStore` instances. The delivered file_id contains
+    the file id marker for :class:`Applicant` object and the
+    registration number or access code of the context applicant. Also
+    the name of the connected applicant container will be part of the
+    generated file id.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for applicant
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for applicants.
+
+    .. seealso:: :mod:`waeup.kofa.imagestorage`
+
+    """
+    grok.context(IApplicant)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        The `attr` parameter is not taken into account for
+        :class:`Applicant` context as the single passport image is the
+        only file we store for applicants.
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, name=None, attr=None):
+        """Get a valid file id for applicant context.
+
+        *Example:*
+
+        For an applicant with applicant_id. ``'app2001_1234'``
+        and stored in an applicants container called
+        ``'mycontainer'``, this chooser would create:
+
+          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
+
+        meaning that the passport image of this applicant would be
+        stored in the site-wide file storage in path:
+
+          ``mycontainer/app2001_1234.jpg``
+
+        If the context applicant has no parent, ``'_default'`` is used
+        as parent name.
+
+        In the beginning the `attr` parameter was not taken into account for
+        :class:`Applicant` context as the single passport image was the
+        only file we store for applicants. Meanwhile many universities require
+        uploads of other documents too. Now we store passport image
+        files without attribute but all other documents with.
+
+        """
+        parent_name = getattr(
+            getattr(self.context, '__parent__', None),
+            '__name__', '_default')
+        if attr is None or attr == 'passport.jpg':
+            marked_filename = '__%s__%s/%s.jpg' % (
+                APPLICANT_IMAGE_STORE_NAME,
+                parent_name, self.context.applicant_id)
+        else:
+            basename, ext = os.path.splitext(attr)
+            marked_filename = '__%s__%s/%s_%s%s' % (
+                APPLICANT_IMAGE_STORE_NAME,
+                parent_name, basename, self.context.applicant_id, ext)
+        return marked_filename
+
+
+class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """Applicant specific image handling.
+
+    This handler knows in which path in a filestore to store applicant
+    images and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__img-applicant__`` (the marker string for applicant images).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(APPLICANT_IMAGE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All applicants images are filed in directory ``applicants``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'applicants')
+        return super(ApplicantImageStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj =  super(
+            ApplicantImageStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, KofaImageFile(
+            file_obj.filename, file_obj.data)
+
+@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
+def handle_applicant_added(applicant, event):
+    """If an applicant is added local and site roles are assigned.
+    """
+    role_manager = IPrincipalRoleManager(applicant)
+    role_manager.assignRoleToPrincipal(
+        'waeup.local.ApplicationOwner', applicant.applicant_id)
+    # Assign current principal the global Applicant role
+    role_manager = IPrincipalRoleManager(grok.getSite())
+    role_manager.assignRoleToPrincipal(
+        'waeup.Applicant', applicant.applicant_id)
+    if applicant.state is None:
+        IWorkflowInfo(applicant).fireTransition('init')
+
+    # Assign global applicant role for new applicant (alternative way)
+    #account = IUserAccount(applicant)
+    #account.roles = ['waeup.Applicant']
+
+    return
+
+@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
+def handle_applicant_removed(applicant, event):
+    """If an applicant is removed a message is logged, passport images are
+    deleted and the global role is unset.
+    """
+    comment = 'Application record removed'
+    target = applicant.applicant_id
+    try:
+        grok.getSite()['applicants'].logger.info('%s - %s' % (
+            target, comment))
+    except KeyError:
+        # If we delete an entire university instance there won't be
+        # an applicants subcontainer
+        return
+    # Remove any passport image.
+    file_store = getUtility(IExtFileStore)
+    file_store.deleteFileByContext(applicant)
+    # Remove all other files too
+    filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
+    for filename in filenames:
+        file_store.deleteFileByContext(applicant, attr=filename[1])
+    # Remove global role
+    role_manager = IPrincipalRoleManager(grok.getSite())
+    role_manager.unsetRoleForPrincipal(
+        'waeup.Applicant', applicant.applicant_id)
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/authentication.py	(revision 16045)
@@ -0,0 +1,123 @@
+## $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
+##
+"""
+Authenticate applicants.
+"""
+import grok
+from zope.component import getUtility
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from waeup.kofa.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.kofa.applicants.interfaces import IApplicant
+from waeup.kofa.students.authentication import (
+    StudentAccount, StudentsAuthenticatorPlugin)
+
+class ApplicantAccount(StudentAccount):
+    """An adapter to turn applicant objects into accounts on-the-fly.
+    """
+    grok.context(IApplicant)
+    grok.implements(IUserAccount)
+
+    @property
+    def name(self):
+        return self.context.applicant_id
+
+    @property
+    def title(self):
+        return self.context.display_fullname
+
+    @property
+    def user_type(self):
+        return u'applicant'
+
+    def checkPassword(self, password):
+        """Check whether the given `password` matches the one stored by
+        students. We additionally check if applicant account has been suspended
+        or if the portal is blocked.
+        """
+        try:
+            blocker = grok.getSite()['configuration'].maintmode_enabled_by
+            if blocker:
+                return False
+        except TypeError:  # in unit tests
+            pass
+        if not isinstance(password, basestring):
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        if not getattr(self.context, 'password', None):
+            # unset/empty passwords do never match
+            return False
+        if self.context.suspended == True:
+            return False
+        return passwordmanager.checkPassword(self.context.password, password)
+
+class ApplicantsAuthenticatorPlugin(StudentsAuthenticatorPlugin):
+    grok.implements(IAuthenticatorPlugin)
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('applicants')
+
+    def getAccount(self, login):
+        """Look up a applicant identified by `login`. Returns an account.
+
+        First we split the login name into the container part and
+        the application number part. Then we simply look up the key under which
+        the applicant is stored in the respective applicants cointainer of
+        the portal.
+
+        Returns not an applicant but an account object adapted from any
+        applicant found.
+
+        If no such applicant exists, ``None`` is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        applicantsroot = site.get('applicants', None)
+        if applicantsroot is None:
+            return None
+        try:
+            container, application_number = login.split('_')
+        except ValueError:
+            return None
+        applicantscontainer = applicantsroot.get(container,None)
+        if applicantscontainer is None:
+            return None
+        applicant = applicantscontainer.get(application_number, None)
+        if applicant is None:
+            return None
+        return IUserAccount(applicant)
+
+class ApplicantsAuthenticatorSetup(grok.GlobalUtility):
+    """Register or unregister applicant authentication for a PAU.
+
+    This piece is called when a new site is created.
+    """
+    grok.implements(IAuthPluginUtility)
+    grok.name('applicants_auth_setup')
+
+    def register(self, pau):
+        plugins = list(pau.authenticatorPlugins)
+        plugins.append('applicants')
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
+
+    def unregister(self, pau):
+        plugins = [x for x in pau.authenticatorPlugins
+                   if x != 'applicants']
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/batching.py	(revision 16045)
@@ -0,0 +1,480 @@
+## $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
+##
+"""Batch processing for applicants.
+"""
+import unicodecsv as csv # XXX: csv ops should move to dedicated module.
+import grok
+from time import time
+from zope.schema import getFields
+from zope.interface import Interface
+from zope.component import queryUtility, getUtility
+from hurry.workflow.interfaces import IWorkflowState
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import (
+    IBatchProcessor, IObjectConverter, FatalCSVError,
+    IGNORE_MARKER, DELETION_MARKER,
+    IObjectHistory, IUserAccount, DuplicationError)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.applicants.interfaces import (
+    IApplicantsContainer, IApplicant, IApplicantUpdateByRegNo,
+    IApplicantOnlinePayment)
+from waeup.kofa.applicants.workflow import  IMPORTABLE_STATES, CREATED
+
+class ApplicantsContainerProcessor(BatchProcessor):
+    """The Applicants Container Processor imports containers for applicants.
+    It does not import their content. There is nothing special about this
+    processor.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantscontainerprocessor'
+    grok.name(util_name)
+
+    name = _('ApplicantsContainer Processor')
+    mode = u'create'
+    iface = IApplicantsContainer
+
+    location_fields = ['code',]
+    factory_name = 'waeup.ApplicantsContainer'
+
+    def parentsExist(self, row, site):
+        return 'applicants' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['code'] in site['applicants'].keys()
+
+    def getParent(self, row, site):
+        return site['applicants']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['code'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent[row['code']] = obj
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        return
+
+class ApplicantProcessor(BatchProcessor):
+    """The Applicant Processor imports application data (applicants).
+
+    In create mode `container_code` is required. If `application_number` is
+    given, an applicant with this number is created in the designated container.
+    If `application_number` is not given, a random `application_number` is
+    assigned. `applicant_id` is being determined by the system and can't be
+    imported.
+
+    In update or remove mode `container_code` and `application_number` columns
+    must not exist. The applicant object is solely localized by searching
+    the applicants catalog for `reg_number` or `applicant_id` .
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantprocessor'
+    grok.name(util_name)
+    name = _('Applicant Processor')
+    iface = IApplicant
+    iface_byregnumber = IApplicantUpdateByRegNo
+    factory_name = 'waeup.Applicant'
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['application_number',
+            'container_code','state','password'] + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        cond1 = 'container_code' in headerfields
+        cond2 = 'application_number' in headerfields
+        cond3 = 'applicant_id' in headerfields
+        cond4 = 'reg_number' in headerfields
+        if mode == 'create':
+            if not cond1:
+                raise FatalCSVError(
+                    "Need at least container_code column!")
+            if cond3:
+                raise FatalCSVError(
+                    "applicant_id can't be imported in create mode!")
+            for field in self.required_fields:
+                if not field in headerfields:
+                    raise FatalCSVError(
+                        "Need at least columns %s for import!" %
+                        ', '.join(["'%s'" % x for x in self.required_fields]))
+        if mode in ('update', 'remove'):
+            if not cond3 and not cond4:
+                raise FatalCSVError(
+                    "Need at least column reg_number or applicant_id!")
+            if cond1 or cond2:
+                raise FatalCSVError(
+                    "container_code or application_number can't be imported " +
+                    "in update or remove mode!")
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def getLocator(self, row):
+        if row.get('container_code', None) not in (IGNORE_MARKER, None):
+            # create mode
+            return 'container_code'
+        elif row.get('applicant_id', None) not in (IGNORE_MARKER, None):
+            # update or remove mode
+            return 'applicant_id'
+        elif row.get('reg_number', None) not in (IGNORE_MARKER, None):
+            # update or remove mode
+            return 'reg_number'
+        else:
+            return None
+
+    def getParent(self, row, site):
+        result = None
+        if self.getLocator(row) == 'container_code':
+            result = site['applicants'].get(row['container_code'], None)
+        elif self.getLocator(row) == 'reg_number':
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                result = results[0].__parent__
+        elif self.getLocator(row) == 'applicant_id':
+            applicant_id = row['applicant_id']
+            cat = queryUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(applicant_id=(applicant_id, applicant_id)))
+            if results:
+                result = results[0].__parent__
+        return result
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def getEntry(self, row, site):
+        if self.getLocator(row) == 'container_code':
+            if row.get('application_number', None) not in (IGNORE_MARKER, None):
+                if not self.parentsExist(row, site):
+                    return None
+                parent = self.getParent(row, site)
+                return parent.get(row['application_number'])
+            return None
+        if self.getLocator(row) == 'applicant_id':
+            applicant_id = row['applicant_id']
+            cat = queryUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(applicant_id=(applicant_id, applicant_id)))
+            if results:
+                return results[0]
+        if self.getLocator(row) == 'reg_number':
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]
+        return None
+
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addApplicant(obj)
+        #parent.__parent__.logger.info(
+        #    'Applicant imported: %s' % obj.applicant_id)
+        history = IObjectHistory(obj)
+        history.addMessage(_('Application record imported'))
+        return
+
+    def delEntry(self, row, site):
+        applicant = self.getEntry(row, site)
+        if applicant is not None:
+            parent = applicant.__parent__
+            del parent[applicant.application_number]
+            #parent.__parent__.logger.info(
+            #    'Applicant removed: %s' % applicant.applicant_id)
+        pass
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = ''
+        # Remove application_number from row if empty
+        if 'application_number' in row and row['application_number'] in (
+            None, IGNORE_MARKER):
+            row.pop('application_number')
+
+        # Update applicant_id fom application_number and container code
+        # if application_number is given
+        if 'application_number' in row:
+            obj.applicant_id = u'%s_%s' % (
+                row['container_code'], row['application_number'])
+            items_changed += ('%s=%s, ' % ('applicant_id', obj.applicant_id))
+            row.pop('application_number')
+
+        # Update 
+        if 'password' in row:
+            passwd = row.get('password', IGNORE_MARKER)
+            if passwd not in ('', IGNORE_MARKER):
+                if passwd.startswith('{SSHA}'):
+                    # already encrypted password
+                    obj.password = passwd
+                elif passwd == DELETION_MARKER:
+                    obj.password = None
+                else:
+                    # not yet encrypted password
+                    IUserAccount(obj).setPassword(passwd)
+                items_changed += ('%s=%s, ' % ('password', passwd))
+            row.pop('password')
+
+        # Update registration state
+        if 'state' in row:
+            state = row.get('state', IGNORE_MARKER)
+            if state not in (IGNORE_MARKER, ''):
+                IWorkflowState(obj).setState(state)
+                msg = _("State '${a}' set", mapping = {'a':state})
+                history = IObjectHistory(obj)
+                history.addMessage(msg)
+                items_changed += ('%s=%s, ' % ('state', state))
+            row.pop('state')
+
+        # apply other values...
+        items_changed += super(ApplicantProcessor, self).updateEntry(
+            obj, row, site, filename)
+
+        # Log actions...
+        parent = self.getParent(row, site)
+        if self.getLocator(row) == 'container_code':
+            parent.__parent__.logger.info(
+                '%s - %s - imported: %s' % (self.name, filename, items_changed))
+        else:
+            parent.__parent__.logger.info(
+                '%s - %s - updated: %s' % (self.name, filename, items_changed))
+        return items_changed
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['applicant_id', 'reg_number'] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+    def checkConversion(self, row, mode='create'):
+        """Validates all values in row.
+        """
+        iface = self.iface
+        if self.getLocator(row) == 'reg_number' or mode == 'remove':
+            iface = self.iface_byregnumber
+        converter = IObjectConverter(iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        cert = conv_dict.get('course1', None)
+        if cert is not None and (mode in ('create', 'update')):
+            # course1 application category must match container's.
+            site = grok.getSite()
+            parent = self.getParent(row, site)
+            if parent is None:
+                errs.append(('container', 'not found'))
+            elif cert.application_category != parent.application_category:
+                errs.append(('course1', 'wrong application category'))
+        if 'state' in row and \
+            not row['state'] in IMPORTABLE_STATES:
+            if row['state'] not in (IGNORE_MARKER, ''):
+                errs.append(('state','not allowed'))
+            else:
+                # state is an attribute of Applicant and must not
+                # be changed if empty
+                conv_dict['state'] = IGNORE_MARKER
+        application_number = row.get('application_number', None)
+        if application_number in (IGNORE_MARKER, ''):
+                conv_dict['application_number'] = IGNORE_MARKER
+        return errs, inv_errs, conv_dict
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        if obj.state == CREATED:
+            return 'Applicant is blocked.'
+        return None
+
+class ApplicantOnlinePaymentProcessor(BatchProcessor):
+    """The Applicant Online Payment Processor imports applicant payment tickets.
+    The tickets are located in the applicant container.
+
+    The `checkConversion` method checks the format of the payment identifier.
+    In create mode it does also ensures that same p_id does not exist
+    elsewhere. It must be portal-wide unique.
+
+    When adding a payment ticket, the `addEntry` method checks if a
+    payment has already been made. If so, a `DuplicationError` is raised.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantpaymentprocessor'
+    grok.name(util_name)
+
+    name = _('ApplicantOnlinePayment Processor')
+    iface = IApplicantOnlinePayment
+    factory_name = 'waeup.ApplicantOnlinePayment'
+
+    location_fields = ['applicant_id',]
+
+    @property
+    def available_fields(self):
+        af = sorted(list(set(
+            self.location_fields + getFields(self.iface).keys())) +
+            ['p_id',])
+        af.remove('display_item')
+        return af
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        super(ApplicantOnlinePaymentProcessor, self).checkHeaders(headerfields)
+        if mode in ('update', 'remove') and not 'p_id' in headerfields:
+            raise FatalCSVError(
+                "Need p_id for import in update and remove modes!")
+        return True
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def getParent(self, row, site):
+        applicant_id = row['applicant_id']
+        cat = queryUtility(ICatalog, name='applicants_catalog')
+        results = list(
+            cat.searchResults(applicant_id=(applicant_id, applicant_id)))
+        if results:
+            return results[0]
+        return None
+
+    def getEntry(self, row, site):
+        applicant = self.getParent(row, site)
+        if applicant is None:
+            return None
+        p_id = row.get('p_id', None)
+        if p_id in (None, IGNORE_MARKER):
+            return None
+        # We can use the hash symbol at the end of p_id in import files
+        # to avoid annoying automatic number transformation
+        # by Excel or Calc
+        p_id = p_id.strip('#')
+        entry = applicant.get(p_id)
+        return entry
+
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(ApplicantOnlinePaymentProcessor, self).updateEntry(
+            obj, row, site, filename)
+        applicant = self.getParent(row, site)
+        applicant.__parent__.__parent__.logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, applicant.applicant_id, items_changed))
+        return
+
+    def samePaymentMade(self, applicant, category):
+        for key in applicant.keys():
+            ticket = applicant[key]
+            if ticket.p_state == 'paid' and\
+                ticket.p_category == category:
+                  return True
+        return False
+
+    def addEntry(self, obj, row, site):
+        applicant = self.getParent(row, site)
+        p_id = row['p_id'].strip('#')
+        if self.samePaymentMade(applicant, obj.p_category):
+            applicant.__parent__.__parent__.logger.info(
+                '%s - %s - previous update cancelled'
+                % (self.name, applicant.applicant_id))
+            raise DuplicationError('Payment has already been made.')
+        applicant[p_id] = obj
+        return
+
+    def delEntry(self, row, site):
+        payment = self.getEntry(row, site)
+        applicant = self.getParent(row, site)
+        if payment is not None:
+            applicant.__parent__.__parent__.logger.info('%s - Payment ticket removed: %s'
+                % (applicant.applicant_id, payment.p_id))
+            del applicant[payment.p_id]
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            ApplicantOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
+        # We have to check p_id.
+        p_id = row.get('p_id', None)
+        if mode == 'create' and p_id in (None, IGNORE_MARKER):
+            timestamp = ("%d" % int(time()*10000))[1:]
+            p_id = "p%s" % timestamp
+            conv_dict['p_id'] = p_id
+            return errs, inv_errs, conv_dict
+        elif p_id in (None, IGNORE_MARKER):
+            errs.append(('p_id','missing'))
+            return errs, inv_errs, conv_dict
+        else:
+            p_id = p_id.strip('#')
+            if not len(p_id) == 14:
+                errs.append(('p_id','invalid length'))
+                return errs, inv_errs, conv_dict
+        if mode == 'create':
+            cat = getUtility(ICatalog, name='payments_catalog')
+            results = list(cat.searchResults(p_id=(p_id, p_id)))
+            if len(results) > 0:
+                sids = [IPayer(payment).id for payment in results]
+                sids_string = ''
+                for id in sids:
+                    sids_string += '%s ' % id
+                errs.append(('p_id','p_id exists in %s' % sids_string))
+                return errs, inv_errs, conv_dict
+        return errs, inv_errs, conv_dict
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser.py	(revision 16045)
@@ -0,0 +1,1760 @@
+## $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 basic applicants and related components.
+"""
+import os
+import sys
+import grok
+import transaction
+from cgi import escape
+from urllib import urlencode
+from datetime import datetime, date
+from time import time, sleep
+from zope.event import notify
+from zope.component import getUtility, queryUtility, createObject, getAdapter
+from zope.catalog.interfaces import ICatalog
+from zope.i18n import translate
+from zope.security import checkPermission
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from reportlab.platypus.doctemplate import LayoutError
+from waeup.kofa.mandates.mandate import RefereeReportMandate
+from waeup.kofa.applicants.interfaces import (
+    IApplicant, IApplicantEdit, IApplicantsRoot,
+    IApplicantsContainer, IApplicantsContainerAdd,
+    IApplicantOnlinePayment, IApplicantsUtils,
+    IApplicantRegisterUpdate, ISpecialApplicant,
+    IApplicantRefereeReport
+    )
+from waeup.kofa.utils.helpers import html2dict
+from waeup.kofa.applicants.container import (
+    ApplicantsContainer, VirtualApplicantsExportJobContainer)
+from waeup.kofa.applicants.applicant import search
+from waeup.kofa.applicants.workflow import (
+    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED)
+from waeup.kofa.browser import (
+#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    DEFAULT_PASSPORT_IMAGE_PATH)
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
+from waeup.kofa.browser.interfaces import ICaptchaManager
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.layout import (
+    NullValidator, jsaction, action, UtilityView)
+from waeup.kofa.browser.pages import (
+    add_local_role, del_local_roles, doll_up, ExportCSVView)
+from waeup.kofa.interfaces import (
+    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF, DOCLINK,
+    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.permissions import get_users_with_local_roles
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
+from waeup.kofa.widgets.datewidget import (
+    FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+
+WARNING = _('You can not edit your application records after final submission.'
+            ' You really want to submit?')
+
+class ApplicantsRootPage(KofaDisplayFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('index')
+    grok.require('waeup.Public')
+    form_fields = grok.AutoFields(IApplicantsRoot)
+    label = _('Applicants Section')
+    pnav = 3
+
+    def update(self):
+        super(ApplicantsRootPage, self).update()
+        return
+
+    @property
+    def introduction(self):
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.description_dict.get(lang,'')
+        if html == '':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.description_dict.get(portal_language,'')
+        return html
+
+    @property
+    def containers(self):
+        if self.layout.isAuthenticated():
+            return self.context.values()
+        values = sorted([container for container in self.context.values()
+                         if not container.hidden and container.enddate],
+                        key=lambda value: value.enddate, reverse=True)
+        return values
+
+class ApplicantsSearchPage(KofaPage):
+    grok.context(IApplicantsRoot)
+    grok.name('search')
+    grok.require('waeup.viewApplication')
+    label = _('Find applicants')
+    search_button = _('Find applicant')
+    pnav = 3
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.results = []
+        if 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type='warning')
+            return
+        self.results = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.results:
+            self.flash(_('No applicant found.'), type='warning')
+        return
+
+class ApplicantsRootManageFormPage(KofaEditFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('manage')
+    grok.template('applicantsrootmanagepage')
+    form_fields = grok.AutoFields(IApplicantsRoot)
+    label = _('Manage applicants section')
+    pnav = 3
+    grok.require('waeup.manageApplication')
+    taboneactions = [_('Save')]
+    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+    subunits = _('Applicants Containers')
+    doclink = DOCLINK + '/applicants.html'
+
+    def getLocalRoles(self):
+        roles = ILocalRolesAssignable(self.context)
+        return roles()
+
+    def getUsers(self):
+        return getUtility(IKofaUtils).getUsers()
+
+    #def getUsers(self):
+    #    """Get a list of all users.
+    #    """
+    #    for key, val in grok.getSite()['users'].items():
+    #        url = self.url(val)
+    #        yield(dict(url=url, name=key, val=val))
+
+    def getUsersWithLocalRoles(self):
+        return get_users_with_local_roles(self.context)
+
+    @jsaction(_('Remove selected'))
+    def delApplicantsContainers(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No container selected!'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            try:
+                del self.context[id]
+                deleted.append(id)
+            except:
+                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
+                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
+        self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+    @action(_('Add applicants container'), validator=NullValidator)
+    def addApplicantsContainer(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3, **data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        description = getattr(self.context, 'description', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.description_dict = html2dict(description, portal_language)
+        self.flash(_('Form has been saved.'))
+        return
+
+class ApplicantsContainerAddFormPage(KofaAddFormPage):
+    grok.context(IApplicantsRoot)
+    grok.require('waeup.manageApplication')
+    grok.name('add')
+    grok.template('applicantscontaineraddpage')
+    label = _('Add applicants container')
+    pnav = 3
+
+    form_fields = grok.AutoFields(
+        IApplicantsContainerAdd).omit('code').omit('title')
+
+    @action(_('Add applicants container'))
+    def addApplicantsContainer(self, **data):
+        year = data['year']
+        if not data['container_number']:
+            code = u'%s%s' % (data['prefix'], year)
+        else:
+            code = u'%s%s' % (data['prefix'], data['container_number'])
+        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
+        title = apptypes_dict[data['prefix']][0]
+        title = u'%s %s/%s' % (title, year, year + 1)
+        if code in self.context.keys():
+            self.flash(
+              _('An applicants container for the same application '
+                'type and entrance year exists already in the database.'),
+                type='warning')
+            return
+        # Add new applicants container...
+        container = createObject(u'waeup.ApplicantsContainer')
+        self.applyData(container, **data)
+        container.code = code
+        container.title = title
+        self.context[code] = container
+        self.flash(_('Added:') + ' "%s".' % code)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info('%s - added: %s' % (ob_class, code))
+        self.redirect(self.url(self.context, u'@@manage'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context, '@@manage'))
+
+class ApplicantsRootBreadcrumb(Breadcrumb):
+    """A breadcrumb for applicantsroot.
+    """
+    grok.context(IApplicantsRoot)
+    title = _(u'Applicants')
+
+class ApplicantsContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for applicantscontainers.
+    """
+    grok.context(IApplicantsContainer)
+
+
+class ApplicantsExportsBreadcrumb(Breadcrumb):
+    """A breadcrumb for exports.
+    """
+    grok.context(VirtualApplicantsExportJobContainer)
+    title = _(u'Applicant Data Exports')
+    target = None
+
+class ApplicantBreadcrumb(Breadcrumb):
+    """A breadcrumb for applicants.
+    """
+    grok.context(IApplicant)
+
+    @property
+    def title(self):
+        """Get a title for a context.
+        """
+        return self.context.application_number
+
+class OnlinePaymentBreadcrumb(Breadcrumb):
+    """A breadcrumb for payments.
+    """
+    grok.context(IApplicantOnlinePayment)
+
+    @property
+    def title(self):
+        return self.context.p_id
+
+class RefereeReportBreadcrumb(Breadcrumb):
+    """A breadcrumb for referee reports.
+    """
+    grok.context(IApplicantRefereeReport)
+
+    @property
+    def title(self):
+        return self.context.r_id
+
+class ApplicantsStatisticsPage(KofaDisplayFormPage):
+    """Some statistics about applicants in a container.
+    """
+    grok.context(IApplicantsContainer)
+    grok.name('statistics')
+    grok.require('waeup.viewApplicationStatistics')
+    grok.template('applicantcontainerstatistics')
+
+    @property
+    def label(self):
+        return "%s" % self.context.title
+
+class ApplicantsContainerPage(KofaDisplayFormPage):
+    """The standard view for regular applicant containers.
+    """
+    grok.context(IApplicantsContainer)
+    grok.name('index')
+    grok.require('waeup.Public')
+    grok.template('applicantscontainerpage')
+    pnav = 3
+
+    @property
+    def form_fields(self):
+        form_fields = grok.AutoFields(IApplicantsContainer).omit(
+            'title', 'description')
+        form_fields[
+            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+        form_fields[
+            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+        if self.request.principal.id == 'zope.anybody':
+            form_fields = form_fields.omit(
+                'code', 'prefix', 'year', 'mode', 'hidden',
+                'strict_deadline', 'application_category',
+                'application_slip_notice', 'with_picture')
+        return form_fields
+
+    @property
+    def introduction(self):
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.description_dict.get(lang,'')
+        if html == '':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.description_dict.get(portal_language,'')
+        return html
+
+    @property
+    def label(self):
+        return "%s" % self.context.title
+
+class ApplicantsContainerManageFormPage(KofaEditFormPage):
+    grok.context(IApplicantsContainer)
+    grok.name('manage')
+    grok.template('applicantscontainermanagepage')
+    form_fields = grok.AutoFields(IApplicantsContainer)
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Remove selected'),_('Cancel'),
+        _('Create students from selected')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+    # Use friendlier date widget...
+    grok.require('waeup.manageApplication')
+    doclink = DOCLINK + '/applicants.html'
+
+    @property
+    def label(self):
+        return _('Manage applicants container')
+
+    pnav = 3
+
+    @property
+    def showApplicants(self):
+        if self.context.counts[1] < 1000:
+            return True
+        return False
+
+    def getLocalRoles(self):
+        roles = ILocalRolesAssignable(self.context)
+        return roles()
+
+    #def getUsers(self):
+    #    """Get a list of all users.
+    #    """
+    #    for key, val in grok.getSite()['users'].items():
+    #        url = self.url(val)
+    #        yield(dict(url=url, name=key, val=val))
+
+    def getUsers(self):
+        return getUtility(IKofaUtils).getUsers()
+
+    def getUsersWithLocalRoles(self):
+        return get_users_with_local_roles(self.context)
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        changed_fields = self.applyData(self.context, **data)
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        description = getattr(self.context, 'description', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.description_dict = html2dict(description, portal_language)
+        self.flash(_('Form has been saved.'))
+        fields_string = ' + '.join(changed_fields)
+        self.context.writeLogMessage(self, 'saved: %s' % fields_string)
+        return
+
+    @jsaction(_('Remove selected'))
+    def delApplicant(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No applicant selected!'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            try:
+                del self.context[id]
+                deleted.append(id)
+            except:
+                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
+                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Create students from selected'))
+    def createStudents(self, **data):
+        if not checkPermission('waeup.createStudents', self.context):
+            self.flash(
+                _('You don\'t have permission to create student records.'),
+                type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No applicant selected!'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        created = []
+        if len(child_id) > 10 and self.request.principal.id != 'admin':
+            self.flash(_('A maximum of 10 applicants can be selected!'),
+                       type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        for id in child_id:
+            success, msg = self.context[id].createStudent(view=self)
+            if success:
+                created.append(id)
+        if len(created):
+            self.flash(_('${a} students successfully created.',
+                mapping = {'a': len(created)}))
+        else:
+            self.flash(_('No student could be created.'), type='warning')
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3, **data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class ApplicantAddFormPage(KofaAddFormPage):
+    """Add-form to add an applicant.
+    """
+    grok.context(IApplicantsContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('addapplicant')
+    #grok.template('applicantaddpage')
+    form_fields = grok.AutoFields(IApplicant).select(
+        'firstname', 'middlename', 'lastname',
+        'email', 'phone')
+    label = _('Add applicant')
+    pnav = 3
+    doclink = DOCLINK + '/applicants.html'
+
+    @action(_('Create application record'))
+    def addApplicant(self, **data):
+        applicant = createObject(u'waeup.Applicant')
+        self.applyData(applicant, **data)
+        self.context.addApplicant(applicant)
+        self.flash(_('Application record created.'))
+        self.redirect(
+            self.url(self.context[applicant.application_number], 'index'))
+        return
+
+class ApplicantsContainerPrefillFormPage(KofaAddFormPage):
+    """Form to pre-fill applicants containers.
+    """
+    grok.context(IApplicantsContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('prefill')
+    grok.template('prefillcontainer')
+    label = _('Pre-fill container')
+    pnav = 3
+    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
+
+    def update(self):
+        if self.context.mode == 'update':
+            self.flash(_('Container must be in create mode to be pre-filled.'),
+                type='danger')
+            self.redirect(self.url(self.context))
+            return
+        super(ApplicantsContainerPrefillFormPage, self).update()
+        return
+
+    @action(_('Pre-fill now'), style='primary')
+    def addApplicants(self):
+        form = self.request.form
+        if 'number' in form and form['number']:
+            number = int(form['number'])
+        for i in range(number):
+            applicant = createObject(u'waeup.Applicant')
+            self.context.addApplicant(applicant)
+        self.flash(_('%s application records created.' % number))
+        self.context.writeLogMessage(self, '%s applicants created' % (number))
+        self.redirect(self.url(self.context, 'index'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class ApplicantsContainerPurgeFormPage(KofaEditFormPage):
+    """Form to purge applicants containers.
+    """
+    grok.context(IApplicantsContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('purge')
+    grok.template('purgecontainer')
+    label = _('Purge container')
+    pnav = 3
+    doclink = DOCLINK + '/applicants/browser.html#preparation-and-maintenance-of-applicants-containers'
+
+    @action(_('Remove initialized records'),
+              tooltip=_('Don\'t use if application is in progress!'),
+              warning=_('Are you really sure?'),
+              style='primary')
+    def purgeInitialized(self):
+        form = self.request.form
+        purged = 0
+        keys = [key for key in self.context.keys()]
+        for key in keys:
+            if self.context[key].state == 'initialized':
+                del self.context[key]
+                purged += 1
+        self.flash(_('%s application records purged.' % purged))
+        self.context.writeLogMessage(self, '%s applicants purged' % (purged))
+        self.redirect(self.url(self.context, 'index'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class ApplicantDisplayFormPage(KofaDisplayFormPage):
+    """A display view for applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('index')
+    grok.require('waeup.viewApplication')
+    grok.template('applicantdisplaypage')
+    label = _('Applicant')
+    pnav = 3
+    hide_hint = False
+
+    @property
+    def file_links(self):
+        html = ''
+        file_store = getUtility(IExtFileStore)
+        additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
+        for filename in additional_files:
+            pdf = getUtility(IExtFileStore).getFileByContext(
+                self.context, attr=filename[1])
+            if pdf:
+                html += '<a href="%s">%s</a>, ' % (self.url(
+                    self.context, filename[1]), filename[0])
+        html = html.strip(', ')
+        return html
+
+    @property
+    def display_payments(self):
+        if self.context.payments:
+            return True
+        if self.context.special:
+            return True
+        return getattr(self.context.__parent__, 'application_fee', None)
+
+    @property
+    def form_fields(self):
+        if self.context.special:
+            form_fields = grok.AutoFields(ISpecialApplicant).omit('locked')
+        else:
+            form_fields = grok.AutoFields(IApplicant).omit(
+                'locked', 'course_admitted', 'password', 'suspended')
+        return form_fields
+
+    @property
+    def target(self):
+        return getattr(self.context.__parent__, 'prefix', None)
+
+    @property
+    def separators(self):
+        return getUtility(IApplicantsUtils).SEPARATORS_DICT
+
+    def update(self):
+        self.passport_url = self.url(self.context, 'passport.jpg')
+        # Mark application as started if applicant logs in for the first time
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if usertype == 'applicant' and \
+            IWorkflowState(self.context).getState() == INITIALIZED:
+            IWorkflowInfo(self.context).fireTransition('start')
+        if usertype == 'applicant' and self.context.state == 'created':
+            session = '%s/%s' % (self.context.__parent__.year,
+                                 self.context.__parent__.year+1)
+            title = getattr(grok.getSite()['configuration'], 'name', u'Sample University')
+            msg = _(
+                '\n <strong>Congratulations!</strong>' +
+                ' You have been offered provisional admission into the' +
+                ' ${c} Academic Session of ${d}.'
+                ' Your student record has been created for you.' +
+                ' Please, logout again and proceed to the' +
+                ' login page of the portal.'
+                ' Then enter your new student credentials:' +
+                ' user name= ${a}, password = ${b}.' +
+                ' Change your password when you have logged in.',
+                mapping = {
+                    'a':self.context.student_id,
+                    'b':self.context.application_number,
+                    'c':session,
+                    'd':title}
+                )
+            self.flash(msg)
+        return
+
+    @property
+    def hasPassword(self):
+        if self.context.password:
+            return _('set')
+        return _('unset')
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return _('${a} <br /> Application Record ${b}', mapping = {
+            'a':container_title, 'b':self.context.application_number})
+
+    def getCourseAdmitted(self):
+        """Return link, title and code in html format to the certificate
+           admitted.
+        """
+        course_admitted = self.context.course_admitted
+        if getattr(course_admitted, '__parent__',None):
+            url = self.url(course_admitted)
+            title = course_admitted.title
+            code = course_admitted.code
+            return '<a href="%s">%s - %s</a>' %(url,code,title)
+        return ''
+
+class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
+    grok.context(IApplicant)
+    grok.name('base')
+
+    @property
+    def form_fields(self):
+        form_fields = grok.AutoFields(IApplicant).select(
+            'applicant_id', 'reg_number', 'email', 'course1')
+        if self.context.__parent__.prefix in ('special',):
+            form_fields['reg_number'].field.title = u'Identification Number'
+            return form_fields
+        return form_fields
+
+class CreateStudentPage(UtilityView, grok.View):
+    """Create a student object from applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('createstudent')
+    grok.require('waeup.createStudents')
+
+    def update(self):
+        success, msg = self.context.createStudent(view=self)
+        if success:
+            self.flash(msg)
+        else:
+            self.flash(msg, type='warning')
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class CreateAllStudentsPage(KofaPage):
+    """Create all student objects from applicant data
+    in the root container or in a specific applicants container only.
+    Only PortalManagers or StudentCreators can do this.
+    """
+    #grok.context(IApplicantsContainer)
+    grok.name('createallstudents')
+    grok.require('waeup.createStudents')
+    label = _('Student Record Creation Report')
+
+    def update(self):
+        grok.getSite()['configuration'].maintmode_enabled_by = u'admin'
+        transaction.commit()
+        # Wait 10 seconds for all transactions to be finished.
+        # Do not wait in tests.
+        if not self.request.principal.id == 'zope.mgr':
+            sleep(10)
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
+        created = []
+        failed = []
+        container_only = False
+        applicants_root = grok.getSite()['applicants']
+        if isinstance(self.context, ApplicantsContainer):
+            container_only = True
+        for result in results:
+            if container_only and result.__parent__ is not self.context:
+                continue
+            success, msg = result.createStudent(view=self)
+            if success:
+                created.append(result.applicant_id)
+            else:
+                failed.append(
+                    (result.applicant_id, self.url(result, 'manage'), msg))
+                ob_class = self.__implemented__.__name__.replace(
+                    'waeup.kofa.','')
+        grok.getSite()['configuration'].maintmode_enabled_by = None
+        self.successful = ', '.join(created)
+        self.failed = failed
+        return
+
+
+class ApplicationFeePaymentAddPage(UtilityView, grok.View):
+    """ Page to add an online payment ticket
+    """
+    grok.context(IApplicant)
+    grok.name('addafp')
+    grok.require('waeup.payApplicant')
+    factory = u'waeup.ApplicantOnlinePayment'
+
+    @property
+    def custom_requirements(self):
+        return ''
+
+    def update(self):
+        # Additional requirements in custom packages.
+        if self.custom_requirements:
+            self.flash(
+                self.custom_requirements,
+                type='danger')
+            self.redirect(self.url(self.context))
+            return
+        if not self.context.special:
+            for key in self.context.keys():
+                ticket = self.context[key]
+                if ticket.p_state == 'paid':
+                      self.flash(
+                          _('This type of payment has already been made.'),
+                          type='warning')
+                      self.redirect(self.url(self.context))
+                      return
+        applicants_utils = getUtility(IApplicantsUtils)
+        container = self.context.__parent__
+        payment = createObject(self.factory)
+        failure = applicants_utils.setPaymentDetails(
+            container, payment, self.context)
+        if failure is not None:
+            self.flash(failure, type='danger')
+            self.redirect(self.url(self.context))
+            return
+        self.context[payment.p_id] = payment
+        self.context.writeLogMessage(self, 'added: %s' % payment.p_id)
+        self.flash(_('Payment ticket created.'))
+        self.redirect(self.url(payment))
+        return
+
+    def render(self):
+        return
+
+
+class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.name('index')
+    grok.require('waeup.viewApplication')
+    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
+    form_fields[
+        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    pnav = 3
+
+    @property
+    def label(self):
+        return _('${a}: Online Payment Ticket ${b}', mapping = {
+            'a':self.context.__parent__.display_fullname,
+            'b':self.context.p_id})
+
+class OnlinePaymentApprovePage(UtilityView, grok.View):
+    """ Approval view
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.name('approve')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        flashtype, msg, log = self.context.approveApplicantPayment()
+        if log is not None:
+            applicant = self.context.__parent__
+            # Add log message to applicants.log
+            applicant.writeLogMessage(self, log)
+            # Add log message to payments.log
+            self.context.logger.info(
+                '%s,%s,%s,%s,%s,,,,,,' % (
+                applicant.applicant_id,
+                self.context.p_id, self.context.p_category,
+                self.context.amount_auth, self.context.r_code))
+        self.flash(msg, type=flashtype)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class ExportPDFPaymentSlipPage(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.name('payment_slip.pdf')
+    grok.require('waeup.viewApplication')
+    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
+    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    prefix = 'form'
+    note = None
+
+    @property
+    def title(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Payment Data'), 'waeup.kofa',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Online Payment Slip'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.p_id
+
+    @property
+    def payment_slip_download_warning(self):
+        if self.context.__parent__.state not in (
+            SUBMITTED, ADMITTED, NOT_ADMITTED, CREATED):
+            return _('Please submit the application form before '
+                     'trying to download payment slips.')
+        return ''
+
+    def render(self):
+        if self.payment_slip_download_warning:
+            self.flash(self.payment_slip_download_warning, type='danger')
+            self.redirect(self.url(self.context))
+            return
+        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(self,'payment_slip.pdf',
+            self.context.__parent__, applicantview, note=self.note)
+
+class ExportPDFPageApplicationSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IApplicant)
+    grok.name('application_slip.pdf')
+    grok.require('waeup.viewApplication')
+    prefix = 'form'
+
+    def update(self):
+        if self.context.state in ('initialized', 'started', 'paid'):
+            self.flash(
+                _('Please pay and submit before trying to download '
+                  'the application slip.'), type='warning')
+            return self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        try:
+            pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
+                view=self)
+        except IOError:
+            self.flash(
+                _('Your image file is corrupted. '
+                  'Please replace.'), type='danger')
+            return self.redirect(self.url(self.context))
+        except LayoutError, err:
+            self.flash(
+                'PDF file could not be created. Reportlab error message: %s'
+                % escape(err.message),
+                type="danger")
+            return self.redirect(self.url(self.context))
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        return pdfstream
+
+def handle_img_upload(upload, context, view):
+    """Handle upload of applicant image.
+
+    Returns `True` in case of success or `False`.
+
+    Please note that file pointer passed in (`upload`) most probably
+    points to end of file when leaving this function.
+    """
+    max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
+    size = file_size(upload)
+    if size > max_upload_size:
+        view.flash(_('Uploaded image is too big!'), type='danger')
+        return False
+    dummy, ext = os.path.splitext(upload.filename)
+    ext.lower()
+    if ext != '.jpg':
+        view.flash(_('jpg file extension expected.'), type='danger')
+        return False
+    upload.seek(0) # file pointer moved when determining size
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName()
+    try:
+        store.createFile(file_id, upload)
+    except IOError:
+        view.flash(_('Image file cannot be changed.'), type='danger')
+        return False
+    return True
+
+def handle_file_upload(upload, context, view, attr=None):
+    """Handle upload of applicant files.
+
+    Returns `True` in case of success or `False`.
+
+    Please note that file pointer passed in (`upload`) most probably
+    points to end of file when leaving this function.
+    """
+    size = file_size(upload)
+    max_upload_size = 1024 * getUtility(IStudentsUtils).MAX_KB
+    if size > max_upload_size:
+        view.flash(_('Uploaded file is too big!'))
+        return False
+    dummy, ext = os.path.splitext(upload.filename)
+    ext.lower()
+    if ext != '.pdf':
+        view.flash(_('pdf file extension expected.'))
+        return False
+    upload.seek(0) # file pointer moved when determining size
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+    store.createFile(file_id, upload)
+    return True
+
+class ApplicantManageFormPage(KofaEditFormPage):
+    """A full edit view for applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('manage')
+    grok.require('waeup.manageApplication')
+    grok.template('applicanteditpage')
+    manage_applications = True
+    pnav = 3
+    display_actions = [[_('Save'), _('Finally Submit')],
+        [_('Add online payment ticket'),_('Remove selected tickets')]]
+
+    @property
+    def display_payments(self):
+        if self.context.payments:
+            return True
+        if self.context.special:
+            return True
+        return getattr(self.context.__parent__, 'application_fee', None)
+
+    @property
+    def display_refereereports(self):
+        if self.context.refereereports:
+            return True
+        return False
+
+    def display_fileupload(self, filename):
+        """This method can be used in custom packages to avoid unneccessary
+        file uploads.
+        """
+        return True
+
+    @property
+    def form_fields(self):
+        if self.context.special:
+            form_fields = grok.AutoFields(ISpecialApplicant)
+            form_fields['applicant_id'].for_display = True
+        else:
+            form_fields = grok.AutoFields(IApplicant)
+            form_fields['student_id'].for_display = True
+            form_fields['applicant_id'].for_display = True
+        return form_fields
+
+    @property
+    def target(self):
+        return getattr(self.context.__parent__, 'prefix', None)
+
+    @property
+    def separators(self):
+        return getUtility(IApplicantsUtils).SEPARATORS_DICT
+
+    @property
+    def custom_upload_requirements(self):
+        return ''
+
+    def update(self):
+        super(ApplicantManageFormPage, self).update()
+        max_upload_size = getUtility(IKofaUtils).MAX_PASSPORT_SIZE
+        self.wf_info = IWorkflowInfo(self.context)
+        self.max_upload_size = string_from_bytes(max_upload_size)
+        self.upload_success = None
+        upload = self.request.form.get('form.passport', None)
+        if upload:
+            if self.custom_upload_requirements:
+                self.flash(
+                    self.custom_upload_requirements,
+                    type='danger')
+                self.redirect(self.url(self.context))
+                return
+            # We got a fresh upload, upload_success is
+            # either True or False
+            self.upload_success = handle_img_upload(
+                upload, self.context, self)
+            if self.upload_success:
+                self.context.writeLogMessage(self, 'saved: passport')
+        file_store = getUtility(IExtFileStore)
+        self.additional_files = getUtility(IApplicantsUtils).ADDITIONAL_FILES
+        for filename in self.additional_files:
+            upload = self.request.form.get(filename[1], None)
+            if upload:
+                # We got a fresh file upload
+                success = handle_file_upload(
+                    upload, self.context, self, attr=filename[1])
+                if success:
+                    self.context.writeLogMessage(
+                        self, 'saved: %s' % filename[1])
+                else:
+                    self.upload_success = False
+        self.max_file_upload_size = string_from_bytes(
+            1024*getUtility(IStudentsUtils).MAX_KB)
+        return
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return _('${a} <br /> Application Form ${b}', mapping = {
+            'a':container_title, 'b':self.context.application_number})
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
+            if not t[0] in ('pay', 'create')]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type='danger')
+                return
+        if self.upload_success is False:  # False is not None!
+            # Error during image upload. Ignore other values.
+            return 
+        changed_fields = self.applyData(self.context, **data)
+        # Turn list of lists into single list
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        if password:
+            # Now we know that the form has no errors and can set password ...
+            IUserAccount(self.context).setPassword(password)
+            changed_fields.append('password')
+        fields_string = ' + '.join(changed_fields)
+        trans_id = form.get('transition', None)
+        if trans_id:
+            self.wf_info.fireTransition(trans_id)
+        self.flash(_('Form has been saved.'))
+        if fields_string:
+            self.context.writeLogMessage(self, 'saved: %s' % fields_string)
+        return
+
+    def unremovable(self, ticket):
+        return False
+
+    # This method is also used by the ApplicantEditFormPage
+    def delPaymentTickets(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No payment selected.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            # Applicants are not allowed to remove used payment tickets
+            if not self.unremovable(self.context[id]):
+                try:
+                    del self.context[id]
+                    deleted.append(id)
+                except:
+                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
+                      id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self, 'removed: % s' % ', '.join(deleted))
+        return
+
+    # We explicitely want the forms to be validated before payment tickets
+    # can be created. If no validation is requested, use
+    # 'validator=NullValidator' in the action directive
+    @action(_('Add online payment ticket'), style='primary')
+    def addPaymentTicket(self, **data):
+        self.redirect(self.url(self.context, '@@addafp'))
+        return
+
+    @jsaction(_('Remove selected tickets'))
+    def removePaymentTickets(self, **data):
+        self.delPaymentTickets(**data)
+        self.redirect(self.url(self.context) + '/@@manage')
+        return
+
+    # Not used in base package
+    def file_exists(self, attr):
+        file = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=attr)
+        if file:
+            return True
+        else:
+            return False
+
+class ApplicantEditFormPage(ApplicantManageFormPage):
+    """An applicant-centered edit view for applicant data.
+    """
+    grok.context(IApplicantEdit)
+    grok.name('edit')
+    grok.require('waeup.handleApplication')
+    grok.template('applicanteditpage')
+    manage_applications = False
+    submit_state = PAID
+
+    @property
+    def display_refereereports(self):
+        return False
+
+    @property
+    def form_fields(self):
+        if self.context.special:
+            form_fields = grok.AutoFields(ISpecialApplicant).omit(
+                'locked', 'suspended')
+            form_fields['applicant_id'].for_display = True
+        else:
+            form_fields = grok.AutoFields(IApplicantEdit).omit(
+                'locked', 'course_admitted', 'student_id',
+                'suspended'
+                )
+            form_fields['applicant_id'].for_display = True
+            form_fields['reg_number'].for_display = True
+        return form_fields
+
+    @property
+    def display_actions(self):
+        state = IWorkflowState(self.context).getState()
+        # If the form is unlocked, applicants are allowed to save the form
+        # and remove unused tickets.
+        actions = [[_('Save')], [_('Remove selected tickets')]]
+        # Only in state started they can also add tickets.
+        if state == STARTED:
+            actions = [[_('Save')],
+                [_('Add online payment ticket'),_('Remove selected tickets')]]
+        # In state paid, they can submit the data and further add tickets
+        # if the application is special.
+        elif self.context.special and state == PAID:
+            actions = [[_('Save'), _('Finally Submit')],
+                [_('Add online payment ticket'),_('Remove selected tickets')]]
+        elif state == PAID:
+            actions = [[_('Save'), _('Finally Submit')],
+                [_('Remove selected tickets')]]
+        return actions
+
+    def unremovable(self, ticket):
+        return ticket.r_code
+
+    def emit_lock_message(self):
+        self.flash(_('The requested form is locked (read-only).'),
+                   type='warning')
+        self.redirect(self.url(self.context))
+        return
+
+    def update(self):
+        if self.context.locked or (
+            self.context.__parent__.expired and
+            self.context.__parent__.strict_deadline):
+            self.emit_lock_message()
+            return
+        super(ApplicantEditFormPage, self).update()
+        return
+
+    def dataNotComplete(self, data):
+        if self.context.__parent__.with_picture:
+            store = getUtility(IExtFileStore)
+            if not store.getFileByContext(self.context, attr=u'passport.jpg'):
+                return _('No passport picture uploaded.')
+            if not self.request.form.get('confirm_passport', False):
+                return _('Passport picture confirmation box not ticked.')
+        return False
+
+    # We explicitely want the forms to be validated before payment tickets
+    # can be created. If no validation is requested, use
+    # 'validator=NullValidator' in the action directive
+    @action(_('Add online payment ticket'), style='primary')
+    def addPaymentTicket(self, **data):
+        self.redirect(self.url(self.context, '@@addafp'))
+        return
+
+    @jsaction(_('Remove selected tickets'))
+    def removePaymentTickets(self, **data):
+        self.delPaymentTickets(**data)
+        self.redirect(self.url(self.context) + '/@@edit')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        if self.upload_success is False:  # False is not None!
+            # Error during image upload. Ignore other values.
+            return
+        self.applyData(self.context, **data)
+        self.flash(_('Form has been saved.'))
+        return
+
+    def informReferees(self):
+        site = grok.getSite()
+        kofa_utils = getUtility(IKofaUtils)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        failed = ''
+        emails_sent = 0
+        for referee in self.context.referees:
+            if referee.email_sent:
+                continue
+            mandate = RefereeReportMandate()
+            mandate.params['name'] = referee.name
+            mandate.params['email'] = referee.email
+            mandate.params[
+                'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
+                    self.context.__parent__.code,
+                    self.context.application_number)
+            site['mandates'].addMandate(mandate)
+            # Send invitation email
+            args = {'mandate_id':mandate.mandate_id}
+            mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+            url_info = u'Report link: %s' % mandate_url
+            success = kofa_utils.inviteReferee(referee, self.context, url_info)
+            if success:
+                emails_sent += 1
+                self.context.writeLogMessage(
+                    self, 'email sent: %s' % referee.email)
+                referee.email_sent = True
+            else:
+                failed += '%s ' % referee.email
+        return failed, emails_sent
+
+    @action(_('Finally Submit'), warning=WARNING)
+    def finalsubmit(self, **data):
+        if self.upload_success is False:  # False is not None!
+            return # error during image upload. Ignore other values
+        dnt = self.dataNotComplete(data)
+        if dnt:
+            self.flash(dnt, type='danger')
+            return
+        self.applyData(self.context, **data)
+        state = IWorkflowState(self.context).getState()
+        # This shouldn't happen, but the application officer
+        # might have forgotten to lock the form after changing the state
+        if state != self.submit_state:
+            self.flash(_('The form cannot be submitted. Wrong state!'),
+                       type='danger')
+            return
+        msg = _('Form has been submitted.')
+        # Create mandates and send emails to referees
+        if getattr(self.context, 'referees', None):
+            failed, emails_sent = self.informReferees()
+            if failed:
+                self.flash(
+                    _('Some invitation emails could not be sent:') + failed,
+                    type='danger')
+                return
+            msg = _('Form has been successfully submitted and '
+                    '${a} invitation emails were sent.',
+                    mapping = {'a':  emails_sent})
+        IWorkflowInfo(self.context).fireTransition('submit')
+        # application_date is used in export files for sorting.
+        # We can thus store utc.
+        self.context.application_date = datetime.utcnow()
+        self.flash(msg)
+        self.redirect(self.url(self.context))
+        return
+
+class PassportImage(grok.View):
+    """Renders the passport image for applicants.
+    """
+    grok.name('passport.jpg')
+    grok.context(IApplicant)
+    grok.require('waeup.viewApplication')
+
+    def render(self):
+        # A filename chooser turns a context into a filename suitable
+        # for file storage.
+        image = getUtility(IExtFileStore).getFileByContext(self.context)
+        self.response.setHeader(
+            'Content-Type', 'image/jpeg')
+        if image is None:
+            # show placeholder image
+            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
+        return image
+
+class ApplicantRegistrationPage(KofaAddFormPage):
+    """Captcha'd registration page for applicants.
+    """
+    grok.context(IApplicantsContainer)
+    grok.name('register')
+    grok.require('waeup.Anonymous')
+    grok.template('applicantregister')
+
+    @property
+    def form_fields(self):
+        form_fields = None
+        if self.context.mode == 'update':
+            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
+                'lastname','reg_number','email')
+        else: #if self.context.mode == 'create':
+            form_fields = grok.AutoFields(IApplicantEdit).select(
+                'firstname', 'middlename', 'lastname', 'email', 'phone')
+        return form_fields
+
+    @property
+    def label(self):
+        return _('Apply for ${a}',
+            mapping = {'a':self.context.title})
+
+    def update(self):
+        if self.context.expired:
+            self.flash(_('Outside application period.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode '
+                        'and registration temporarily disabled.'),
+                       type='warning')
+            self.redirect(self.url(self.context))
+            return
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    def _redirect(self, email, password, applicant_id):
+        # Forward only email to landing page in base package.
+        self.redirect(self.url(self.context, 'registration_complete',
+            data = dict(email=email)))
+        return
+
+    @property
+    def _postfix(self):
+        """In customized packages we can add a container dependent string if
+        applicants have been imported into several containers.
+        """
+        return ''
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def register(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        if self.context.mode == 'create':
+            # Check if there are unused records in this container which
+            # can be taken
+            applicant = self.context.first_unused
+            if applicant is None:
+                # Add applicant
+                applicant = createObject(u'waeup.Applicant')
+                self.context.addApplicant(applicant)
+            else:
+                applicants_root = grok.getSite()['applicants']
+                ob_class = self.__implemented__.__name__.replace(
+                    'waeup.kofa.','')
+                applicants_root.logger.info('%s - used: %s' % (
+                    ob_class, applicant.applicant_id))
+            self.applyData(applicant, **data)
+            # applicant.reg_number = applicant.applicant_id
+            notify(grok.ObjectModifiedEvent(applicant))
+        elif self.context.mode == 'update':
+            # Update applicant
+            reg_number = data.get('reg_number','')
+            lastname = data.get('lastname','')
+            cat = getUtility(ICatalog, name='applicants_catalog')
+            searchstr = reg_number + self._postfix
+            results = list(
+                cat.searchResults(reg_number=(searchstr, searchstr)))
+            if results:
+                applicant = results[0]
+                if getattr(applicant,'lastname',None) is None:
+                    self.flash(_('An error occurred.'), type='danger')
+                    return
+                elif applicant.lastname.lower() != lastname.lower():
+                    # Don't tell the truth here. Anonymous must not
+                    # know that a record was found and only the lastname
+                    # verification failed.
+                    self.flash(
+                        _('No application record found.'), type='warning')
+                    return
+                elif applicant.password is not None and \
+                    applicant.state != INITIALIZED:
+                    self.flash(_('Your password has already been set and used. '
+                                 'Please proceed to the login page.'),
+                               type='warning')
+                    return
+                # Store email address but nothing else.
+                applicant.email = data['email']
+                notify(grok.ObjectModifiedEvent(applicant))
+            else:
+                # No record found, this is the truth.
+                self.flash(_('No application record found.'), type='warning')
+                return
+        else:
+            # Does not happen but anyway ...
+            return
+        kofa_utils = getUtility(IKofaUtils)
+        password = kofa_utils.genPassword()
+        IUserAccount(applicant).setPassword(password)
+        # Send email with credentials
+        login_url = self.url(grok.getSite(), 'login')
+        url_info = u'Login: %s' % login_url
+        msg = _('You have successfully been registered for the')
+        if kofa_utils.sendCredentials(IUserAccount(applicant),
+            password, url_info, msg):
+            email_sent = applicant.email
+        else:
+            email_sent = None
+        self._redirect(email=email_sent, password=password,
+            applicant_id=applicant.applicant_id)
+        return
+
+class ApplicantRegistrationEmailSent(KofaPage):
+    """Landing page after successful registration.
+
+    """
+    grok.name('registration_complete')
+    grok.require('waeup.Public')
+    grok.template('applicantregemailsent')
+    label = _('Your registration was successful.')
+
+    def update(self, email=None, applicant_id=None, password=None):
+        self.email = email
+        self.password = password
+        self.applicant_id = applicant_id
+        return
+
+class ApplicantCheckStatusPage(KofaPage):
+    """Captcha'd status checking page for applicants.
+    """
+    grok.context(IApplicantsRoot)
+    grok.name('checkstatus')
+    grok.require('waeup.Anonymous')
+    grok.template('applicantcheckstatus')
+    buttonname = _('Submit')
+
+    def label(self):
+        if self.result:
+            return _('Admission status of ${a}',
+                     mapping = {'a':self.applicant.applicant_id})
+        return _('Check your admission status')
+
+    def update(self, SUBMIT=None):
+        form = self.request.form
+        self.result = False
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        if SUBMIT:
+            if not self.captcha_result.is_valid:
+                # Captcha will display error messages automatically.
+                # No need to flash something.
+                return
+            unique_id = form.get('unique_id', None)
+            lastname = form.get('lastname', None)
+            if not unique_id or not lastname:
+                self.flash(
+                    _('Required input missing.'), type='warning')
+                return
+            cat = getUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(applicant_id=(unique_id, unique_id)))
+            if not results:
+                results = list(
+                    cat.searchResults(reg_number=(unique_id, unique_id)))
+            if results:
+                applicant = results[0]
+                if applicant.lastname.lower().strip() != lastname.lower():
+                    # Don't tell the truth here. Anonymous must not
+                    # know that a record was found and only the lastname
+                    # verification failed.
+                    self.flash(
+                        _('No application record found.'), type='warning')
+                    return
+            else:
+                self.flash(_('No application record found.'), type='warning')
+                return
+            self.applicant = applicant
+            self.entry_session = "%s/%s" % (
+                applicant.__parent__.year,
+                applicant.__parent__.year+1)
+            course_admitted = getattr(applicant, 'course_admitted', None)
+            self.course_admitted = False
+            if course_admitted is not None:
+                try:
+                    self.course_admitted = True
+                    self.longtitle = course_admitted.longtitle
+                    self.department = course_admitted.__parent__.__parent__.longtitle
+                    self.faculty = course_admitted.__parent__.__parent__.__parent__.longtitle
+                except AttributeError:
+                    self.flash(_('Application record invalid.'), type='warning')
+                    return
+            self.result = True
+            self.admitted = False
+            self.not_admitted = False
+            self.submitted = False
+            self.not_submitted = False
+            self.created = False
+            if applicant.state in (ADMITTED, CREATED):
+                self.admitted = True
+            if applicant.state in (CREATED):
+                self.created = True
+                self.student_id = applicant.student_id
+                self.password = applicant.application_number
+            if applicant.state in (NOT_ADMITTED,):
+                self.not_admitted = True
+            if applicant.state in (SUBMITTED,):
+                self.submitted = True
+            if applicant.state in (INITIALIZED, STARTED, PAID):
+                self.not_submitted = True
+        return
+
+class ExportJobContainerOverview(KofaPage):
+    """Page that lists active applicant data export jobs and provides links
+    to discard or download CSV files.
+
+    """
+    grok.context(VirtualApplicantsExportJobContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('index.html')
+    grok.template('exportjobsindex')
+    label = _('Data Exports')
+    pnav = 3
+
+    def update(self, CREATE=None, DISCARD=None, job_id=None):
+        if CREATE:
+            self.redirect(self.url('@@start_export'))
+            return
+        if DISCARD and job_id:
+            entry = self.context.entry_from_job_id(job_id)
+            self.context.delete_export_entry(entry)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - discarded: job_id=%s' % (ob_class, job_id))
+            self.flash(_('Discarded export') + ' %s' % job_id)
+        self.entries = doll_up(self, user=self.request.principal.id)
+        return
+
+class ExportJobContainerJobStart(UtilityView, grok.View):
+    """View that starts two export jobs, one for applicants and a second
+    one for applicant payments.
+    """
+    grok.context(VirtualApplicantsExportJobContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('start_export')
+
+    def update(self):
+        utils = queryUtility(IKofaUtils)
+        if not utils.expensive_actions_allowed():
+            self.flash(_(
+                "Currently, exporters cannot be started due to high "
+                "system load. Please try again later."), type='danger')
+            self.entries = doll_up(self, user=None)
+            return
+
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        container_code = self.context.__parent__.code
+        # Start first exporter
+        exporter = 'applicants'
+        job_id = self.context.start_export_job(exporter,
+                                      self.request.principal.id,
+                                      container=container_code)
+        self.context.logger.info(
+            '%s - exported: %s (%s), job_id=%s'
+            % (ob_class, exporter, container_code, job_id))
+        # Commit transaction so that job is stored in the ZODB
+        transaction.commit()
+        # Start second exporter
+        exporter = 'applicantpayments'
+        job_id = self.context.start_export_job(exporter,
+                                      self.request.principal.id,
+                                      container=container_code)
+        self.context.logger.info(
+            '%s - exported: %s (%s), job_id=%s'
+            % (ob_class, exporter, container_code, job_id))
+
+        self.flash(_('Exports started.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class ExportJobContainerDownload(ExportCSVView):
+    """Page that downloads a students export csv file.
+
+    """
+    grok.context(VirtualApplicantsExportJobContainer)
+    grok.require('waeup.manageApplication')
+
+class RefereeReportDisplayFormPage(KofaDisplayFormPage):
+    """A display view for referee reports.
+    """
+    grok.context(IApplicantRefereeReport)
+    grok.name('index')
+    grok.require('waeup.manageApplication')
+    label = _('Referee Report')
+    pnav = 3
+
+class RefereeReportAddFormPage(KofaAddFormPage):
+    """Add-form to add an referee report. This form
+    is protected by a mandate.
+    """
+    grok.context(IApplicant)
+    grok.require('waeup.Public')
+    grok.name('addrefereereport')
+    form_fields = grok.AutoFields(
+        IApplicantRefereeReport).omit('creation_date')
+    grok.template('refereereportpage')
+    label = _('Add referee report')
+    pnav = 3
+    #doclink = DOCLINK + '/refereereports.html'
+
+    def update(self):
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode. '
+                        'Referee report forms are temporarily disabled.'),
+                       type='warning')
+            self.redirect(self.application_url())
+            return
+        # Check mandate
+        form = self.request.form
+        self.mandate_id = form.get('mandate_id', None)
+        self.mandates = grok.getSite()['mandates']
+        mandate = self.mandates.get(self.mandate_id, None)
+        if mandate is None and not self.request.form.get('form.actions.submit'):
+            self.flash(_('No mandate.'), type='warning')
+            self.redirect(self.application_url())
+            return
+        if mandate:
+            # Prefill form with mandate params
+            self.form_fields.get(
+                'name').field.default = mandate.params['name']
+            self.form_fields.get(
+                'email').field.default = mandate.params['email']
+        super(RefereeReportAddFormPage, self).update()
+        return
+
+    @action(_('Submit'),
+              warning=_('Are you really sure? '
+                        'Reports can neither be modified or added '
+                        'after submission.'),
+              style='primary')
+    def addRefereeReport(self, **data):
+        report = createObject(u'waeup.ApplicantRefereeReport')
+        timestamp = ("%d" % int(time()*10000))[1:]
+        report.r_id = "r%s" % timestamp
+        self.applyData(report, **data)
+        self.context[report.r_id] = report
+        self.flash(_('Referee report has been saved. Thank you!'))
+        self.context.writeLogMessage(self, 'added: %s' % report.r_id)
+        # Delete mandate
+        del self.mandates[self.mandate_id]
+        self.redirect(self.application_url())
+        return
+
+class AdditionalFile(grok.View):
+    """Renders additional pdf files for applicants.
+    This is a baseclass.
+    """
+    grok.baseclass()
+    grok.context(IApplicant)
+    grok.require('waeup.viewApplication')
+
+    def render(self):
+        pdf = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.__name__)
+        self.response.setHeader('Content-Type', 'application/pdf')
+        return pdf
+
+class TestFile(AdditionalFile):
+    """Renders testfile.pdf.
+    """
+    grok.name('testfile.pdf')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt	(revision 16045)
@@ -0,0 +1,98 @@
+<form method="post" enctype="multipart/form-data"
+    tal:condition="not:view/result" i18n:domain="waeup.kofa">
+<br />
+  <table class="form-table">
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Registration Number or Applicant Id:
+      </td>
+      <td>
+        <input class="textType half" type="text" name="unique_id" />
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Lastname (Surname):
+      </td>
+      <td>
+        <input class="textType half" type="text" name="lastname" />
+      </td>
+    </tr>
+  </table>
+  <div tal:content="structure view/captcha_code"></div>
+  <br />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+      tal:attributes="value view/buttonname" />
+</form>
+
+<div tal:condition="view/result" i18n:domain="waeup.kofa">
+  <br />
+  <tal:admitted condition="view/admitted">
+    <p i18n:translate="app_admitted">
+      <strong>Congratulations!</strong> You have been offered provisional
+      admission into the <span  i18n:name="entry_session" tal:replace="view/entry_session"></span>
+      Academic Session of <span  i18n:name="app_title" tal:replace="layout/getAppTitle"></span>.
+    </p>
+    <span tal:condition="view/course_admitted">
+      <table class="form-table">
+        <tr>
+          <td style="width: 150px;" i18n:translate="">Name:
+          </td>
+          <td><span tal:replace="view/applicant/display_fullname"></span>
+          </td>
+        </tr>
+        <tr>
+          <td style="width: 150px;" i18n:translate="">Study Course:
+          </td>
+          <td><span tal:replace="view/longtitle"></span>
+          </td>
+        </tr>
+        <tr>
+          <td i18n:translate=""> Department:
+          </td>
+          <td><span tal:replace="view/department"></span>
+          </td>
+        </tr>
+        <tr>
+          <td i18n:translate="">Faculty:
+          </td>
+          <td><span tal:replace="view/faculty"></span>
+          </td>
+        </tr>
+      </table>
+    </span>
+  </tal:admitted>
+
+  <tal:created condition="view/created">
+    <br />
+    <p i18n:translate="appl_created">
+      Your student record has already been created for you. Your new
+      user name (= student id) is: <strong><span  i18n:name="student_id"
+      tal:replace="view/student_id">STUDENT_ID</span></strong>.
+      Your initial password is: <strong><span  i18n:name="appl_number"
+      tal:replace="view/password">PASSWORD</span></strong>.
+      Please proceed to the <a  i18n:name="login_url"
+      tal:attributes="href python: view.url(layout.site, 'login')">login form</a>,
+      enter your new login credentials and change your password immediately after login.
+    </p>
+  </tal:created>
+
+  <tal:notadmitted condition="view/not_admitted">
+    <p i18n:translate="">
+      Sorry, you have not been offered admission.
+    </p>
+  </tal:notadmitted>
+
+  <tal:submitted condition="view/submitted">
+    <p i18n:translate="">
+      Your submitted application is being processed.
+    </p>
+  </tal:submitted>
+
+  <tal:notsubmitted condition="view/not_submitted">
+    <p i18n:translate="">
+      You have not yet submitted your application.
+    </p>
+  </tal:notsubmitted>
+
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt	(revision 16045)
@@ -0,0 +1,33 @@
+<tal:statistics define="stats context/statistics" i18n:domain="waeup.kofa">
+  <h2 i18n:translate="">Container Statistics</h2>
+  <table class="table">
+    <thead>
+      <tr>
+        <th i18n:translate="" width="40%">Application State</th>
+        <th i18n:translate="">Number of Applicants</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="state python:('initialized',
+          'started','paid','submitted','processed','admitted','not admitted',
+          'created')">
+        <td tal:content="state">STATE</td>
+        <td tal:content="python: stats[0][state] ">NUMBER</td>
+      </tr>
+    </tbody>
+  </table>
+  <table class="table" tal:condition="python: stats[1]">
+    <thead>
+      <tr>
+        <th i18n:translate="" width="40%">Faculty</th>
+        <th i18n:translate="">Number of Submissions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="fac python:stats[1].keys()">
+        <td tal:content="fac">FAC</td>
+        <td tal:content="python: stats[1][fac] ">NUMBER</td>
+      </tr>
+    </tbody>
+  </table>
+</tal:statistics>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt	(revision 16045)
@@ -0,0 +1,80 @@
+<h2 i18n:domain="waeup.kofa" tal:condition="context/translated_state">
+  ...
+  <span tal:replace="context/translated_state">APPLICATIONSTATE
+  </span>
+  <span tal:omit-tag=""
+    i18n:translate="" tal:condition="context/suspended">(account suspended)
+  </span>
+  ...
+</h2>
+
+<div class="workflow" tal:condition="context/history">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<img tal:condition="python: context.__parent__.with_picture"
+     src="" height="180px" tal:attributes="src view/passport_url" />
+
+<table i18n:domain="waeup.kofa" class="form-table">
+  <tbody>
+    <tal:files condition="view/file_links">
+      <tr>
+        <td class="separator" colspan=2>
+          Files
+        </td>
+      </tr>
+      <tr>
+        <td colspan=2>
+          <span tal:replace="structure view/file_links" />
+        </td>
+      </tr>
+    </tal:files>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="view/getCourseAdmitted">
+      <td class="fieldname" i18n:translate="">
+          Admitted Course of Study:
+      </td>
+      <td>
+        <span tal:replace="structure view/getCourseAdmitted" />
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    </tr>
+  </tbody>
+</table>
+<tal:payments condition="view/display_payments">
+  <h3 i18n:domain="waeup.kofa" i18n:translate="">
+      Payment Tickets
+  </h3>
+  <table i18n:domain="waeup.kofa" class="table table-condensed">
+    <thead>
+      <tr>
+        <th i18n:translate="">Payment Id</th>
+        <th i18n:translate="">Creation Date</th>
+        <th i18n:translate="">Payment Date</th>
+        <th i18n:translate="">Category</th>
+        <th i18n:translate="">Item</th>
+        <th i18n:translate="">State</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="value context/payments">
+        <td> <a tal:attributes="href python:view.url(value)">
+        <span tal:content="value/p_id">PID</span></a></td>
+        <td tal:content="python: layout.formatDatetime(value.creation_date)">CREATION DATE</td>
+        <td tal:content="python: layout.formatDatetime(value.payment_date)">PAYMENT DATE</td>
+        <td tal:content ="value/category">CATEGORY</td>
+        <td tal:content ="value/display_item">ITEM</td>
+        <td tal:content ="value/p_state_title">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+</tal:payments>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt	(revision 16045)
@@ -0,0 +1,197 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <h2 i18n:domain="waeup.kofa" tal:condition="context/translated_state">
+    ...
+    <span tal:replace="context/translated_state">APPLICATIONSTATE
+    </span>
+    <span tal:omit-tag=""
+      i18n:translate="" tal:condition="context/suspended">(account suspended)
+    </span>
+    ...
+  </h2>
+
+  <div class="workflow" tal:condition="context/history">
+    <div tal:repeat="msg context/history/messages">
+      <span tal:replace="msg">MESSAGE</span><br />
+    </div>
+  </div>
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr tal:condition="python: context.__parent__.with_picture">
+        <td class="fieldname" i18n:translate="">
+          Passport Photo:
+        </td>
+        <td>
+          <img src="passport.jpg" height="180px" /><br />
+          <br />
+      		<div class="input-group half">
+      			<div class="input-group-btn">
+      				<div class="btn btn-default btn-file">
+                Select&hellip;
+                <input type="file" name="form.passport" />
+              </div>
+      			</div>
+      			<input type="text" class="form-control" readonly>
+          </div>
+          <span i18n:translate="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_upload_size">10 KB</span>
+      </tr>
+
+      <tal:files>
+        <tr tal:repeat="filename view/additional_files">
+          <tal:cond tal:condition="python:view.display_fileupload(filename)">
+            <td class="fieldname">
+              <span tal:replace="python:filename[0]">FILENAME</span>:
+            </td>
+            <td>
+              <p tal:condition="python:view.file_exists(filename[1])">
+                <a tal:attributes="href python:filename[1]"i18n:translate="">
+                  Download pdf file
+                </a>
+              </p>
+              <div class="input-group half">
+                <div class="input-group-btn">
+                  <div class="btn btn-default btn-file">
+                    Select&hellip;
+                  <input type="file" tal:attributes="name python:filename[1]" />
+                  </div>
+                </div>
+                <input type="text" class="form-control" readonly>
+              </div>
+              <span i18n:translate="">
+                PDF files only. Max. file size:
+              </span>
+              <span tal:replace="view/max_file_upload_size">10 KB</span>
+            </td>
+          </tal:cond>
+        </tr>
+      </tal:files>
+
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input class="form-control" name="password" type="password"  />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input class="form-control" name="control_password" type="password" />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Application Transition:</td>
+        <td>
+          <select id="transition" name="transition" class="form-control half">
+            <option tal:repeat="transition view/getTransitions"
+                    tal:attributes="value transition/name">
+              <span tal:replace="transition/title">TRANSITIONTITLE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="python: not view.manage_applications
+                              and context.__parent__.with_picture">
+    <input id="confirm_passport" name="confirm_passport"
+           type="checkbox" value="True"/>&nbsp;&nbsp;
+    <span i18n:translate="">
+    I confirm that the Passport Photograph uploaded on this form is a
+    true picture of me.
+    </span>
+  </div>
+  <br />
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:condition="python:action.label in view.display_actions[0]"
+             tal:replace="structure action/render"/>
+    </span>
+  </div>
+
+  <tal:refereereports condition="view/display_refereereports">
+    <br /><br />
+    <h3 i18n:translate="">
+        Referee Reports
+    </h3>
+    <table i18n:domain="waeup.kofa" class="table table-condensed">
+      <thead>
+      <tr>
+        <th i18n:translate="">Report Id</th>
+        <th i18n:translate="">Creation Date</th>
+        <th i18n:translate="">Referee</th>
+        <th i18n:translate="">Email Address</th>
+      </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="cl context/refereereports">
+          <td> <a tal:attributes="href python:view.url(cl)">
+            <span tal:content="cl/r_id">RID</span></a></td>
+          <td tal:content="python: layout.formatDatetime(cl.creation_date)">CREATION DATE</td>
+          <td tal:content="cl/name">REFEREE</td>
+          <td tal:content="cl/email">EMAIL</td>
+        </tr>
+      </tbody>
+    </table>
+  </tal:refereereports>
+
+  <tal:payments condition="view/display_payments">
+    <br /><br />
+    <h3 i18n:translate="">
+        Payment Tickets
+    </h3>
+
+    <table i18n:domain="waeup.kofa" class="table table-condensed">
+      <thead>
+      <tr>
+        <th>&nbsp;</th>
+        <th i18n:translate="">Payment Id</th>
+        <th i18n:translate="">Creation Date</th>
+        <th i18n:translate="">Payment Date</th>
+        <th i18n:translate="">Category</th>
+        <th i18n:translate="">Item</th>
+        <th i18n:translate="">State</th>
+      </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="cl context/payments">
+          <td>
+           <input type="checkbox"  name="val_id"
+           tal:attributes="value cl/__name__"
+           tal:condition="python: not view.unremovable(cl)" />
+          </td>
+          <td> <a tal:attributes="href python:view.url(cl)">
+          <span tal:content="cl/p_id">PID</span></a></td>
+          <td tal:content="python: layout.formatDatetime(cl.creation_date)">CREATION DATE</td>
+          <td tal:content="python: layout.formatDatetime(cl.payment_date)">PAYMENT DATE</td>
+          <td tal:content ="cl/category">CATEGORY</td>
+          <td tal:content ="cl/display_item">ITEM</td>
+          <td tal:content ="cl/p_state_title">STATE</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <div tal:condition="view/availableActions">
+      <span tal:repeat="action view/actions"
+            tal:omit-tag="">
+        <input tal:condition="python:action.label in view.display_actions[1]"
+               tal:replace="structure action/render"/>
+      </span>
+    </div>
+    <br />
+    <p i18n:translate="" tal:condition="context/special">
+      <strong>Notice:</strong> If you need to pay for another service,
+      select the corresponding special application category above,
+      click 'Save' and then 'Add online payment ticket' again.
+    </p>
+  </tal:payments>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt	(revision 16045)
@@ -0,0 +1,47 @@
+<tal:only_email condition = "not: view/password" i18n:domain="waeup.kofa">
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p tal:condition = "view/email" i18n:translate="">
+    Please check your email account to proceed.
+  </p>
+  <p tal:condition = "not: view/email" i18n:translate="">
+    Email could not been sent. Please retry later.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:only_email>
+
+<tal:credentials condition = "view/password" i18n:domain="waeup.kofa">
+  <h3 i18n:translate="">Your login credentials are:</h3>
+  <table class="table-bordered">
+    <tr>
+      <td i18n:translate="">User Name:</td>
+      <td tal:content="view/applicant_id">ID</td>
+    </tr>
+    <tr>
+      <td i18n:translate="">Password:</td>
+      <td tal:content="view/password">PASSWORD</td>
+    </tr>
+  </table>
+  <p>
+    <span i18n:translate="">Print this page and proceed to the</span>
+    <a tal:attributes="href python: view.url(layout.site, 'login')">login form</a>.
+    <span i18n:translate="">Please note that passwords are case-sensitive,
+    <br />when entering your credentials, and keep your password secret!
+    </span>
+  </p>
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:credentials>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregister.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregister.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantregister.pt	(revision 16045)
@@ -0,0 +1,24 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <p i18n:translate="" tal:condition="python: context.mode=='create'">
+    An account will be created for you and an email with your login
+    credentials will be sent to the address provided.
+  </p>
+  <p i18n:translate="" tal:condition="python: context.mode=='update'">
+    Your application record will be looked up and an email with your login
+    credentials will be sent to the address provided.
+  </p>
+  <div tal:content="structure view/captcha_code"></div>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 16045)
@@ -0,0 +1,15 @@
+<form action="." tal:attributes="action request/URL" method="post"
+  enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 16045)
@@ -0,0 +1,165 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+ <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+    <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab2" data-toggle="tab">
+      <span i18n:translate="">Applicants
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab3" data-toggle="tab">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div  tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+    <br />
+    This container contains <span tal:replace="python: context.counts[0]-context.counts[1]">UNUSED</span> unused pre-filled records.<br />
+    <tal:showApplicants  condition="view/showApplicants">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Application Number
+            </th>
+            <th i18n:translate="">Full Name
+            </th>
+            <th i18n:translate="">First Choice
+            </th>
+            <th i18n:translate="">Application State
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="appl python:[appl for appl in context.values() if appl.container_code.endswith('+')]">
+            <td>
+              <input type="checkbox" name="val_id" tal:attributes="value appl/__name__" />
+            </td>
+            <td>
+              <a tal:attributes="href python: view.url(appl)"
+                 tal:content="appl/__name__">NUMBER</a>
+            </td>
+            <td>
+              <span tal:replace="appl/display_fullname">FULLNAME
+              </span>
+            </td>
+            <td>
+              <span tal:replace="appl/course1/code|nothing">FIRSTCHOICE
+              </span>
+            </td>
+            <td>
+              <span tal:replace="appl/translated_state">STATE
+              </span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <input type="checkbox" onClick="toggle(this, 'val_id')" />
+      &nbsp;<span  i18n:translate="">Select all</span>
+      <br /><br />
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </tal:showApplicants>
+    <tal:showNoApplicants condition="not: view/showApplicants">
+      <p i18n:translate="note_acmp">
+          There are more than 1000 application records in this container.
+          In order to prevent from downloading big amounts of data, the
+          Applicants tab has been disabled. Please use the
+          <span i18n:name="search_link">
+            <a tal:attributes="href python: view.url(layout.site, 'applicants/search')"
+               i18n:translate="">
+                search form
+            </a>
+          </span>
+          for accessing application records.
+      </p>
+    </tal:showNoApplicants>
+    </div>
+    <div id="tab3" class="tab-pane"> <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+              <input type="checkbox" name="role_id"
+                tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME  </td>
+            <td tal:content="entry/user_title"> USERTITLE  </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE  </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                  tal:replace="structure action/render"/>
+        </span>
+      </div> <br /><br />
+      <table class="form-table">
+        <tr>
+          <td>
+            <select id="user" name="user" class="form-control">
+              <option tal:repeat="user view/getUsers"
+                      tal:attributes="value user/name">
+              <span tal:replace="user/val">USERSTRING</span>
+              </option>
+            </select>
+          </td>
+          <td>
+            <select id="local_role" name="local_role" class="form-control">
+              <option tal:repeat="localrole view/getLocalRoles"
+                      tal:attributes="value localrole/name">
+              <span tal:replace="localrole/title">LOCALROLETITLE</span>
+              </option>
+            </select>
+          </td>
+          <td>
+            <div tal:condition="view/availableActions">
+              <span tal:repeat="action view/actions" tal:omit-tag="">
+                <input tal:condition="python:action.label in view.tabthreeactions2"
+                       tal:replace="structure action/render"/>
+              </span>
+            </div>
+          </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt	(revision 16045)
@@ -0,0 +1,18 @@
+<table class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <tal:block replace="python:widget.label"/>:
+        </td>
+        <td>
+          <tal:block tal:replace="structure widget" />
+        </td>
+      </tr>
+    </tal:block>
+  </tbody>
+</table>
+
+<span tal:condition="view/introduction" tal:omit-tag="">
+  <p tal:content="structure view/introduction">INTRODUCTION</p>
+</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 16045)
@@ -0,0 +1,140 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST"  enctype="multipart/form-data">
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+    <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Introduction
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab2" data-toggle="tab">
+      <span tal:content="view/subunits">Contents
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab3" data-toggle="tab">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+     <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+            tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <p i18n:translate="note_armp">
+        Please note that containers will be opened in manage mode here.
+        All application record links will be loaded into your browser which
+        can take a considerable time depending on your bandwidth.
+        Please be patient until all records have been loaded.
+      </p>
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Year
+            </th>
+            <th i18n:translate="">Code
+            </th>
+            <th i18n:translate="">Title
+            </th>
+            <th i18n:translate="">Records
+            </th>
+            <th i18n:translate="">Application Deadline
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry context/values"> <td>
+              <input type="checkbox" name="val_id"
+                tal:attributes="value entry/__name__" /> </td>
+            <td tal:content="entry/year">YEAR</td> <td>
+              <a href=""
+                tal:attributes="href python:view.url(entry) + '/manage'"
+                tal:content="entry/__name__">CODE</a> </td>
+            <td tal:content="entry/title">TITLE</td>
+            <td tal:content="python: len(entry)">RECORDS</td>
+            <td><span tal:content="python: layout.formatDatetime(entry.enddate)">
+                END</span></td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+            tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles">
+            <td> <input type="checkbox" name="role_id"
+                    tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" />  </td>
+            <td tal:content="entry/user_name">USERNAME</td>
+            <td tal:content="entry/user_title">USERTITLE</td>
+            <td tal:content="entry/local_role_title">LOCAL ROLE</td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+            tal:replace="structure action/render"/>
+        </span>
+      </div> <br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group" tal:condition="view/availableActions">
+          <span tal:repeat="action view/actions" tal:omit-tag="">
+            <input tal:condition="python:action.label in view.tabthreeactions2"
+                   tal:replace="structure action/render"/>
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt	(revision 16045)
@@ -0,0 +1,35 @@
+<span tal:condition="view/introduction" tal:omit-tag="">
+  <span tal:content="structure view/introduction">INTRODUCTION</span>
+  <br />
+</span>
+
+<div class="table-responsive">
+  <table i18n:domain="waeup.kofa" class="table table-hover">
+    <thead>
+      <tr>
+        <th i18n:translate="">Year</th>
+        <th i18n:translate="">Code</th>
+        <th i18n:translate="">Title</th>
+        <th i18n:translate="">Application Period</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="entry view/containers">
+        <td tal:content="entry/year">Year
+        </td>
+        <td tal:content="entry/__name__">CODE
+        </td>
+        <td>
+  	     <a href=""
+      	   tal:attributes="href python:view.url(entry)"
+      	   tal:content="entry/title">TITLE</a>
+        </td>
+        <td>
+        <span tal:content="python: layout.formatTZDate(entry.startdate)">START</span>
+        -
+        <span tal:content="python: layout.formatTZDate(entry.enddate)">END</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt	(revision 16045)
@@ -0,0 +1,49 @@
+<form method="POST" i18n:domain="waeup.kofa" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="applicant_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="email">with email address</option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+
+  <br /><br />
+  <div tal:condition="view/results">
+    <h3 i18n:translate="">Search Results</h3>
+    <input type="hidden" name="old_searchterm"
+         tal:attributes="value view/searchterm" />
+    <input type="hidden" name="old_searchtype"
+         tal:attributes="value view/searchtype" />
+    <table class="kofa-data-table dataTable">
+      <thead>
+      <tr>
+        <th i18n:translate="">Applicant Id</th>
+        <th i18n:translate="">Reg. Number</th>
+        <th i18n:translate="">Application State</th>
+        <th i18n:translate="">Full Name</th>
+      </tr>
+      </thead>
+      <tbody>
+      <tr tal:repeat="item view/results">
+        <td> <a tal:attributes="href python:view.url(item)">
+          <span tal:content="item/applicant_id">XXXX_1234</span></a>
+        </td>
+        <td class="center">
+          <span tal:content="item/reg_number">9999999</span>
+        </td>
+        <td tal:content="item/state">admitted</td>
+        <td tal:content="item/display_fullname">Bob</td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/createallstudentspage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/createallstudentspage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/createallstudentspage.pt	(revision 16045)
@@ -0,0 +1,21 @@
+<div i18n:domain="waeup.kofa">
+  <tal:success condition="view/successful">
+    <strong i18n:translate="">Successfully created records:</strong>
+    <br />
+    <div  tal:replace="structure view/successful">
+    </div>
+    <br />
+  </tal:success>
+  <tal:fail condition="view/failed">
+    <strong i18n:translate="">Failed records:</strong>
+    <br />
+    <div  tal:repeat="value view/failed">
+      <a tal:attributes="href python:value[1]" tal:content="python:value[0]"> ID</a>
+      (<span tal:content="python:value[2]">MSG</span>)
+    </div>
+  </tal:fail>
+  <tal:norecord i18n:translate=""
+       condition="python:not view.failed and not view.successful">
+      No record found.
+  </tal:norecord>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt	(revision 16045)
@@ -0,0 +1,59 @@
+<table i18n:domain="waeup.kofa" class="table table-condensed table-hover">
+  <thead>
+    <tr>
+      <th i18n:translate="">Export Number</th>
+      <th i18n:translate="">Exporter</th>
+      <th i18n:translate="">Filter</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="job view/entries">
+      <td>
+        <span tal:replace="job/job">12</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/exporter">EXPORTER</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/args">ARGS</span>
+      </td>
+      <td>
+        <span tal:replace="job/creator">CREATOR</span>
+      </td>
+      <td>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn btn-default btn-xs" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img src="/static/img/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn btn-primary btn-xs"
+                 tal:attributes="href job/download_url"
+                 tal:condition="job/show_download_button">
+            Download</a>
+          <input type="hidden" name="job_id"
+                 tal:attributes="value job/job" />
+          <input type="submit" class="btn btn-default btn-xs"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<form method="POST" i18n:domain="waeup.kofa">
+  <input class="btn btn-primary" type="submit" name="CREATE"
+         value="Start new exports"
+          />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/prefillcontainer.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/prefillcontainer.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/prefillcontainer.pt	(revision 16045)
@@ -0,0 +1,31 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+    <br />
+    <p i18n:translate="prefill">
+        <strong>Technical Note:</strong> An application in create mode can be
+        performed with an empty container. Each time an applicant registers,
+        a new record is being created and a corresponding user account
+        set up. Since each single account creation causes a significant
+        growth of Kofa's database, it is strongly recommended to pre-fill
+        applicants containers with empty application records. This is done
+        in a single transaction and does thus save database volume when
+        application is ongoing.
+    </p>
+    <br />
+    <label i18n:translate="">
+     Pre-fill '<span  i18n:name="code" tal:replace="context/code">CODE</span>' container with
+    </label>
+    <select id="number" name="number" class="form-control half">
+      <option value="10" i18n:translate="">10 empty records</option>
+      <option value="100" i18n:translate="">100 empty records</option>
+      <option value="1000" i18n:translate="">1000 empty records</option>
+      <option value="10000" i18n:translate="">10000 empty records</option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/purgecontainer.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/purgecontainer.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/purgecontainer.pt	(revision 16045)
@@ -0,0 +1,24 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+    <br />
+    <p i18n:translate="purge">
+        <strong>Technical Note:</strong> During application, many records
+        are being initialized but not used. For various reasons, many
+        applicants do create one or more application records with
+        corresponding user accounts but fail to log in. These records
+        remain in state 'initialized' and are never turned to state
+        'started'. After a while, these records can be safely removed.
+        <br /><br />
+        <strong>Attention:</strong> When purging an applicants container, also
+        all unused pre-filled application records are being removed.
+        If necessary, the container must be pre-filled with empty
+        records again.
+    </p>
+    <br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/refereereportpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/refereereportpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/browser_templates/refereereportpage.pt	(revision 16045)
@@ -0,0 +1,23 @@
+<p  i18n:domain="waeup.kofa" i18n:translate="referee_welcome">
+  You have been invited to report about
+  <span  i18n:name="applicant_name" tal:replace="context/display_fullname">
+  </span>.
+  After submission the form, will be blocked.
+  You can neither view nor edit your report after submission.
+</p>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <input type="hidden" name="mandate_id" tal:attributes="value view/mandate_id" />
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/container.py	(revision 16045)
@@ -0,0 +1,145 @@
+## $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
+##
+"""
+Containers for university applicants.
+"""
+from random import SystemRandom as r
+import grok
+import pytz
+from datetime import datetime
+import zope.location.location
+from zope.component import getUtility, ComponentLookupError
+from zope.component.factory import Factory
+from zope.component.interfaces import IFactory
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.applicants.interfaces import (
+    IApplicantsContainer, IApplicantsContainerAdd, IApplicant,
+    IApplicantsUtils)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+
+def generate_applicant_id(container=None):
+    if container is not None:
+        key = r().randint(99999,1000000)
+        while str(key) in container.keys():
+            key = r().randint(99999,1000000)
+        return u"%s_%d" % (container.code, key)
+    else:
+        # In some tests we don't use containers
+        return u"xxx_1234"
+
+class VirtualApplicantsExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for applicants container.
+    """
+
+class ApplicantsContainer(grok.Container):
+    """An applicants container contains university applicants.
+    """
+    grok.implements(IApplicantsContainer,IApplicantsContainerAdd)
+
+    description_dict = {}
+
+    local_roles = [
+        'waeup.local.ApplicationsManager',
+        ]
+
+    def addApplicant(self, applicant):
+        """Add an applicant.
+        """
+        if not IApplicant.providedBy(applicant):
+            raise TypeError(
+                'ApplicantsContainers contain only IApplicant instances')
+        if applicant.applicant_id is None:
+            applicant_id = generate_applicant_id(container=self)
+            applicant.applicant_id = applicant_id
+        self[applicant.application_number] = applicant
+        return
+
+    @property
+    def counts(self):
+        total = len(self)
+        code = self.code + '+'
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(
+           cat.searchResults(container_code=(code, code)))
+        return len(self), len(results)
+
+    @property
+    def first_unused(self):
+        code = self.code + '-'
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(
+           cat.searchResults(container_code=(code, code)))
+        if results:
+            return results[0]
+        return
+
+    @property
+    def statistics(self):
+        try:
+          statistics = getUtility(
+              IApplicantsUtils).getApplicantsStatistics(self)
+        except ComponentLookupError:  # happens in unit tests
+            return
+        return statistics
+
+    @property
+    def expired(self):
+        # Check if application has started ...
+        if not self.startdate or (
+            self.startdate > datetime.now(pytz.utc)):
+            return True
+        # ... or ended
+        if not self.enddate or (
+            self.enddate < datetime.now(pytz.utc)):
+            return True
+        return False
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.code, message))
+        return
+
+    def traverse(self, name):
+        """Deliver virtual export container.
+        """
+        if name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualApplicantsExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+ApplicantsContainer = attrs_to_fields(ApplicantsContainer)
+
+# ApplicantsContainers must be importable. So we need a factory.
+class ApplicantsContainerFactory(grok.GlobalUtility):
+    """A factory for student online payments.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.ApplicantsContainer')
+    title = u"Create a new container for applicants.",
+    description = u"This factory instantiates new IApplicantsContainer instances."
+
+    def __call__(self, *args, **kw):
+        return ApplicantsContainer()
+
+    def getInterfaces(self):
+        return implementedBy(ApplicantsContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/dynamicroles.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/dynamicroles.py	(revision 16045)
@@ -0,0 +1,105 @@
+## $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
+##
+"""Security policy components for applicants.
+
+Applicants need special security policy treatment, as officers with
+local roles for departments and faculties might have additional
+permissions (local roles on depts/faculties) here.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.principalrole import AnnotationPrincipalRoleManager
+from waeup.kofa.applicants.interfaces import IApplicant
+
+# All components in here have the same context: Applicant instances
+grok.context(IApplicant)
+
+class ApplicantPrincipalRoleManager(AnnotationPrincipalRoleManager,
+                                    grok.Adapter):
+    grok.provides(IPrincipalRoleManager)
+
+    #: The attribute name to lookup for additional roles
+    extra_attrib = 'course1'
+
+    # Role name mapping:
+    # role name to look for in `extra_attrib` and parents
+    # to
+    # role to add in case this role was found
+    rolename_mapping = {
+        'waeup.local.ApplicationsManager':'waeup.ApplicationsManager',
+        }
+
+    def getRolesForPrincipal(self, principal_id):
+        """Get roles for principal with id `principal_id`.
+
+        Different to the default implementation, this method also
+        takes into account local roles set on any department connected
+        to the context student.
+
+        If the given principal has at least one of the
+        `external_rolenames` roles granted for the external object, it
+        additionally gets `additional_rolename` role for the context
+        student.
+
+        For the additional roles the `extra_attrib` and all its parent
+        objects are looked up, because 'role inheritance' does not
+        work on that basic level of permission handling.
+
+        Some advantages of this approach:
+
+        - we don't have to store extra local roles for clearance
+          officers in ZODB for each student
+
+        - when local roles on a department change, we don't have to
+          update thousands of students; the local role is assigned
+          dynamically.
+
+        Disadvantage:
+
+        - More expensive role lookups when a clearance officer wants
+          to see an student form.
+
+        This implementation is designed to be usable also for other
+        contexts than students. You can inherit from it and set
+        different role names to lookup/set easily via the static class
+        attributes.
+        """
+        apr_manager = AnnotationPrincipalRoleManager(self._context)
+        result = apr_manager.getRolesForPrincipal(principal_id)
+        if result != []:
+            # If there are local roles defined here, no additional
+            # lookup is done.
+            return result
+        # The principal has no local roles yet. Let's lookup the
+        # connected course, dept, etc.
+        obj = getattr(self._context, self.extra_attrib, None)
+        # Lookup local roles for connected course and all parent
+        # objects. This way we fake 'role inheritance'.
+        while obj is not None:
+            extra_roles = IPrincipalRoleManager(obj).getRolesForPrincipal(
+                principal_id)
+            for role_id, setting in extra_roles:
+                if role_id in self.rolename_mapping.keys():
+                    # Grant additional role
+                    # permissions (allow, deny or unset)
+                    # according to the rolename mapping above.
+                    result.append(
+                        (self.rolename_mapping[role_id], setting))
+                    return result
+            obj = getattr(obj, '__parent__', None)
+        return result
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/export.py	(revision 16045)
@@ -0,0 +1,220 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for applicant-related stuff.
+"""
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.kofa.applicants.interfaces import (
+    IApplicantBaseData, IApplicantsContainer, IApplicantOnlinePayment)
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names
+
+class ApplicantsContainerExporter(grok.GlobalUtility, ExporterBase):
+    """The Applicants Container Exporter exports container data. It does not
+    export applicants (application records) inside the container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('applicantscontainers')
+
+    fields = tuple(sorted(iface_names(IApplicantsContainer)))
+    title = _(u'Applicants Containers')
+
+    def mangle_value(self, value, name, context=None):
+        return super(
+            ApplicantsContainerExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export(self, containers, filepath=None):
+        """Export `containers`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for container in containers:
+            self.write_item(container, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export applicantscontainer into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        containers = site.get('applicants', {})
+        return self.export(containers.values(), filepath)
+
+
+class ApplicantExporter(grok.GlobalUtility, ExporterBase):
+    """The Applicant Exporter exports application records (= applicants)
+    stored in the database. In contrast to the exporters in the academic
+    section this exporter does not iterate over the items of containers
+    but searches the :class:`ApplicantsCatalog` instead.
+
+    The exporter exports all applicants if started in the Data Center
+    which means in the context of the `DataCenter` object. The exporter can also 
+    be started 'locally' which means in the context of an `ApplicantsContainer`
+    container. Then the :meth:`export_filtered()` instead of the 
+    :meth:`export_all()` method is applied which searches for applicants
+    in the respective container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('applicants')
+
+    fields = tuple(sorted(iface_names(IApplicantBaseData))) + (
+        'password', 'state', 'history', 'container_code', 'application_number',
+        'display_fullname', 'application_date')
+    title = _(u'Applicants')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the codes of the atributes `course1`,
+        `course2` and `course_admitted`. It furthermore prepares the
+        history messages and adds a hash symbol at the end of the phone number
+        to avoid annoying automatic number transformation by Excel or Calc.
+        """
+        if name.startswith('course') and value is not None:
+            value = value.code
+        #elif name == 'school_grades':
+        #    value = [eval(entry.to_string()) for entry in value]
+        elif name == 'history':
+            value = getattr(value, 'messages', None)
+        elif name == 'phone' and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        elif name == 'container_code':
+            value = value.strip('+')
+        return super(
+            ApplicantExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export(self, applicants, filepath=None):
+        """Export `applicants`, an iterable, as CSV file.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for applicant in applicants:
+            self.write_item(applicant, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export all applicants into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        Only used records are being exported.
+        """
+        catalog = queryUtility(
+            ICatalog, context=site, name='applicants_catalog', default=None)
+        if catalog is None:
+            return self.export([], filepath)
+        applicants = catalog.searchResults(
+            # reg_num might not be set and then would not be found.
+            # We therefore search for applicant_id.
+            applicant_id=(None, None))
+        used = [value for value in applicants
+                if value.container_code.endswith('+')]
+        return self.export(used, filepath=filepath)
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export filtered applicants in container denoted by keywords (`kw`).
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned. Only used records are being exported.
+        """
+        container = grok.getSite()['applicants'][kw['container']]
+        container_values = container.values()
+        used = [value for value in container_values
+                if value.container_code.endswith('+')]
+        return self.export(used, filepath=filepath)
+
+
+class ApplicantPaymentExporter(grok.GlobalUtility, ExporterBase):
+    """The Applicant Payments Exporter exports all payments made by applicants.
+    In other words, it exports payment tickets in state 'paid'. The exporter
+    searches :class:`ApplicantsCatalog` and iterates over all payment tickets
+    which are stored in an applicant (container).
+
+    The exporter exports all applicant payments if started in the Data Center
+    which means in the context of the `DataCenter` object. The exporter can also
+    be started 'locally' which means in the context of an
+    `ApplicantsContainer` container, see `ApplicantExporter` above.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('applicantpayments')
+
+    fields = tuple(sorted(iface_names(
+        IApplicantOnlinePayment,
+        exclude_attribs=False,
+        omit=['display_item']))) + (
+              'applicant_id',
+              'reg_number',
+              'display_fullname',)
+    title = _(u'Applicant Payments')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the applicant's id.
+        """
+        if name in ('applicant_id', 'reg_number',
+            'display_fullname',) and context is not None:
+            applicant = context.__parent__
+            value = getattr(applicant, name, None)
+        return super(
+            ApplicantPaymentExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export(self, payments, filepath=None):
+        """
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for payment in payments:
+            self.write_item(payment, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """
+        """
+        catalog = queryUtility(
+            ICatalog, context=site, name='applicants_catalog', default=None)
+        if catalog is None:
+            return self.export([], filepath)
+        applicants = catalog.searchResults(
+            # reg_num might not be set and then would not be found.
+            # We therefore search for applicant_id.
+            applicant_id=(None, None))
+        used = [value for value in applicants
+                if value.container_code.endswith('+')]
+        payments = []
+        for applicant in used:
+            for payment in applicant.payments:
+                if payment.p_state == 'paid':
+                    payments.append(payment)
+        return self.export(payments, filepath=filepath)
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """
+        """
+        container = grok.getSite()['applicants'][kw['container']]
+        container_values = container.values()
+        used = [value for value in container_values
+                if value.container_code.endswith('+')]
+        payments = []
+        for applicant in used:
+            for payment in applicant.payments:
+                if payment.p_state == 'paid':
+                    payments.append(payment)
+        return self.export(payments, filepath=filepath)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/interfaces.py	(revision 16045)
@@ -0,0 +1,695 @@
+## $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
+##
+"""Interfaces of the university application package.
+"""
+from datetime import datetime
+from grokcore.content.interfaces import IContainer
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from zope import schema
+from zope.component import queryUtility, getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.interface import Interface, Attribute, implements, directlyProvides
+from zope.schema.interfaces import (
+    ValidationError, ISource, IContextSourceBinder)
+from waeup.kofa.schema import TextLineChoice, FormattedDate
+from waeup.kofa.interfaces import (
+    IKofaObject, validate_email, validate_html,
+    SimpleKofaVocabulary)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.payments.interfaces import IOnlinePayment
+from waeup.kofa.schema import PhoneNumber
+from waeup.kofa.schoolgrades import ResultEntryField
+from waeup.kofa.refereeentries import RefereeEntryField
+from waeup.kofa.students.vocabularies import GenderSource, RegNumberSource
+from waeup.kofa.university.vocabularies import (
+    AppCatSource, CertificateSource, SpecialApplicationSource)
+
+_marker = object() # a marker different from None
+
+def year_range():
+    curr_year = datetime.now().year
+    return range(curr_year - 4, curr_year + 5)
+
+class RegNumInSource(ValidationError):
+    """Registration number exists already
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class ApplicantRegNumberSource(RegNumberSource):
+    """A source that accepts any reg number if not used already by a
+    different applicant.
+    """
+    cat_name = 'applicants_catalog'
+    field_name = 'reg_number'
+    validation_error = RegNumInSource
+    comp_field = 'applicant_id'
+
+def contextual_reg_num_source(context):
+    source = ApplicantRegNumberSource(context)
+    return source
+directlyProvides(contextual_reg_num_source, IContextSourceBinder)
+
+
+class AppCatCertificateSource(CertificateSource):
+    """An application certificate source delivers all certificates
+    which belong to a certain application_category.
+
+    This source is meant to be used with Applicants.
+
+    The application category must match the application category of
+    the context parent, normally an applicants container.
+    """
+    def contains(self, context, value):
+        context_appcat = getattr(getattr(
+            context, '__parent__', None), 'application_category', _marker)
+        if context_appcat is _marker:
+            # If the context (applicant) has no application category,
+            # then it might be not part of a container (yet), for
+            # instance during imports. We consider this correct.
+            return True
+        if value.application_category == context_appcat:
+            return True
+        return False
+
+    def getValues(self, context):
+        appcat = getattr(getattr(context, '__parent__', None),
+                         'application_category', None)
+        catalog = getUtility(ICatalog, name='certificates_catalog')
+        result = catalog.searchResults(
+            application_category=(appcat,appcat))
+        resultlist = getUtility(
+            IApplicantsUtils).sortCertificates(context, result)
+        return resultlist
+
+    def getTitle(self, context, value):
+        return getUtility(
+            IApplicantsUtils).getCertTitle(context, value)
+
+class ApplicationTypeSource(BasicContextualSourceFactory):
+    """An application type source delivers screening types defined in the
+    portal.
+    """
+    def getValues(self, context):
+        appcats_dict = getUtility(
+            IApplicantsUtils).APP_TYPES_DICT
+        return [item[0] for item in sorted(appcats_dict.items(),
+                                           key=lambda item: item[1])]
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        appcats_dict = getUtility(
+            IApplicantsUtils).APP_TYPES_DICT
+        return appcats_dict[value][0]
+
+# Maybe FUTMinna still needs this ...
+#class ApplicationPinSource(BasicContextualSourceFactory):
+#    """An application pin source delivers PIN prefixes for application
+#    defined in the portal.
+#    """
+#    def getValues(self, context):
+#        apppins_dict = getUtility(
+#            IApplicantsUtils).APP_TYPES_DICT
+#        return sorted(appcats_dict.keys())
+#
+#    def getToken(self, context, value):
+#        return value
+#
+#    def getTitle(self, context, value):
+#        apppins_dict = getUtility(
+#            IApplicantsUtils).APP_TYPES_DICT
+#        return u"%s (%s)" % (
+#            apppins_dict[value][1],self.apppins_dict[value][0])
+
+application_modes_vocab = SimpleKofaVocabulary(
+    (_('Create Application Records'), 'create'),
+    (_('Update Application Records'), 'update'),
+    )
+
+class IApplicantsUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+    APP_TYPES_DICT = Attribute('dict of application types')
+
+    def setPaymentDetails(container, payment):
+        """Set the payment data of an applicant.
+        """
+
+    def getApplicantsStatistics(container):
+        """Count applicants in containers.
+        """
+
+    def filterCertificates(context, resultset):
+        """Filter and sort certificates for AppCatCertificateSource.
+        """
+
+    def getCertTitle(context, value):
+        """Compose the titles in AppCatCertificateSource.
+        """
+
+class IApplicantsRoot(IKofaObject, IContainer):
+    """A container for applicants containers.
+    """
+    description_dict = Attribute('Language translation dictionary with values in HTML format')
+    local_roles = Attribute('List of local role names')
+    logger_name = Attribute('Name of the logger')
+    logger_filename = Attribute('Name of the logger file')
+
+    description = schema.Text(
+        title = _(u'Human readable description in HTML format'),
+        required = False,
+        constraint=validate_html,
+        default = u'''This text can been seen by anonymous users.
+Here we put multi-lingual general information about the application procedure.
+>>de<<
+Dieser Text kann von anonymen Benutzern gelesen werden.
+Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
+        )
+
+class IApplicantsContainer(IKofaObject):
+    """An applicants container contains applicants.
+    """
+    statistics = Attribute('Applicant counts')
+    expired = Attribute('True if application has started but not ended')
+
+    description_dict = Attribute('Language translation dictionary with values in HTML format')
+    local_roles = Attribute('List of local role names')
+
+
+    code = schema.TextLine(
+        title = _(u'Code'),
+        required = True,
+        readonly = True,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        required = True,
+        readonly = False,
+        )
+
+    prefix = schema.Choice(
+        title = _(u'Application Target'),
+        required = True,
+        source = ApplicationTypeSource(),
+        readonly = True,
+        )
+
+    year = schema.Choice(
+        title = _(u'Year of Entrance'),
+        required = True,
+        values = year_range(),
+        readonly = True,
+        )
+
+    mode = schema.Choice(
+        title = _(u'Application Mode'),
+        vocabulary = application_modes_vocab,
+        required = True,
+        )
+
+    # Maybe FUTMinna still needs this ...
+    #ac_prefix = schema.Choice(
+    #    title = u'Activation code prefix',
+    #    required = True,
+    #    default = None,
+    #    source = ApplicationPinSource(),
+    #    )
+
+    application_category = schema.Choice(
+        title = _(u'Category for the grouping of certificates'),
+        required = True,
+        source = AppCatSource(),
+        )
+
+    description = schema.Text(
+        title = _(u'Human readable description in HTML format'),
+        required = False,
+        constraint=validate_html,
+        default = u'''This text can been seen by anonymous users.
+Here we put multi-lingual information about the study courses provided, the application procedure and deadlines.
+>>de<<
+Dieser Text kann von anonymen Benutzern gelesen werden.
+Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
+        )
+
+    startdate = schema.Datetime(
+        title = _(u'Application Start Date'),
+        required = False,
+        description = _('Example: ') + u'2011-12-01 18:30:00+01:00',
+        )
+
+    enddate = schema.Datetime(
+        title = _(u'Application Closing Date'),
+        required = False,
+        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
+        )
+
+    strict_deadline = schema.Bool(
+        title = _(u'Forbid additions after deadline (enddate)'),
+        required = False,
+        default = True,
+        )
+
+    application_fee = schema.Float(
+        title = _(u'Application Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    application_slip_notice = schema.Text(
+        title = _(u'Human readable notice on application slip in HTML format'),
+        required = False,
+        constraint=validate_html,
+        )
+
+    hidden= schema.Bool(
+        title = _(u'Hide container'),
+        required = False,
+        default = False,
+        )
+
+    with_picture= schema.Bool(
+        title = _(u'With passport picture'),
+        required = False,
+        default = True,
+        )
+
+    def addApplicant(applicant):
+        """Add an applicant.
+        """
+
+    def writeLogMessage(view, comment):
+        """Add an INFO message to applicants.log.
+        """
+
+    def traverse(name):
+        """Deliver appropriate containers.
+        """
+
+class IApplicantsContainerAdd(IApplicantsContainer):
+    """An applicants container contains university applicants.
+    """
+    prefix = schema.Choice(
+        title = _(u'Application Target'),
+        required = True,
+        source = ApplicationTypeSource(),
+        readonly = False,
+        )
+
+    year = schema.Choice(
+        title = _(u'Year of Entrance'),
+        required = True,
+        values = year_range(),
+        readonly = False,
+        )
+
+    container_number = schema.Choice(
+        title = _(u'Container Number'),
+        values = range(1,100),
+        description = _(u'If set, this number will be added to the container '
+                         'prefix (e.g. app3). If not set, the year of entrance will be '
+                         'used (e.g. app2019).'),
+        required = False,
+        readonly = False,
+        )
+
+IApplicantsContainerAdd[
+    'prefix'].order =  IApplicantsContainer['prefix'].order
+IApplicantsContainerAdd[
+    'year'].order =  IApplicantsContainer['year'].order
+IApplicantsContainerAdd[
+    'container_number'].order =  IApplicantsContainer['year'].order
+
+class IApplicantBaseData(IKofaObject):
+    """This is a base interface of an applicant with no field
+    required. For use with processors, forms, etc., please use one of
+    the derived interfaces below, which set more fields to required
+    state, depending on use-case.
+    """
+    state = Attribute('Application state of an applicant')
+    history = Attribute('Object history, a list of messages')
+    display_fullname = Attribute('The fullname of an applicant')
+    application_number = Attribute('The key under which the record is stored')
+    container_code = Attribute('Code of the parent container plus additional information if record is used or not')
+    translated_state = Attribute('Real name of the application state')
+    special = Attribute('True if special application')
+    payments = Attribute('List of payment objects stored in the applicant container')
+
+    application_date = Attribute('UTC datetime of submission, used for export only')
+    password = Attribute('Encrypted password of an applicant')
+
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        default = False,
+        required = False,
+        )
+
+    applicant_id = schema.TextLine(
+        title = _(u'Applicant Id'),
+        required = False,
+        readonly = False,
+        )
+
+    reg_number = TextLineChoice(
+        title = _(u'Registration Number'),
+        readonly = False,
+        required = True,
+        source = contextual_reg_num_source,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    middlename = schema.TextLine(
+        title = _(u'Middle Name'),
+        required = False,
+        )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = False,
+        show_year = True,
+        )
+
+    sex = schema.Choice(
+        title = _(u'Gender'),
+        source = GenderSource(),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = False,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+
+    course1 = schema.Choice(
+        title = _(u'1st Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        )
+    student_id = schema.TextLine(
+        title = _(u'Student Id'),
+        required = False,
+        readonly = False,
+        )
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        required = False,
+        )
+    locked = schema.Bool(
+        title = _(u'Form locked'),
+        default = False,
+        required = False,
+        )
+
+    special_application = schema.Choice(
+        title = _(u'Special Application'),
+        source = SpecialApplicationSource(),
+        required = False,
+        )
+
+class IApplicantTestData(IKofaObject):
+    """This interface is for demonstration and testing only.
+    It can be omitted in customized versions of Kofa.
+    """
+
+    school_grades = schema.List(
+        title = _(u'School Grades'),
+        value_type = ResultEntryField(),
+        required = False,
+        defaultFactory=list,
+        )
+
+    referees = schema.List(
+        title = _(u'Referees'),
+        value_type = RefereeEntryField(),
+        required = False,
+        defaultFactory=list,
+        )
+
+IApplicantTestData['school_grades'].order = IApplicantBaseData['course2'].order
+
+class IApplicant(IApplicantBaseData, IApplicantTestData):
+    """This is basically the applicant base data. Here we repeat the
+    fields from base data if we have to set the `required` attribute
+    to True (which is the default).
+    """
+
+    def writeLogMessage(view, comment):
+        """Add an INFO message to applicants.log.
+        """
+
+    def createStudent():
+        """Create a student object from applicant data and copy
+        passport image and application slip.
+        """
+
+class ISpecialApplicant(IKofaObject):
+    """This reduced interface is for former students or students who are not
+    users of the portal but have to pay supplementary fees.
+    This interface is used in browser components only. Thus we can't add
+    fields here to the regular IApplicant interface here. We can
+    only 'customize' fields.
+    """
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        default = False,
+        required = False,
+        )
+
+    locked = schema.Bool(
+        title = _(u'Form locked'),
+        default = False,
+        required = False,
+        )
+
+    applicant_id = schema.TextLine(
+        title = _(u'Applicant Id'),
+        required = False,
+        readonly = False,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    middlename = schema.TextLine(
+        title = _(u'Middle Name'),
+        required = False,
+        )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    reg_number = TextLineChoice(
+        title = _(u'Identification Number'),
+        #description = _(u'Enter either registration or matriculation number.'),
+        readonly = False,
+        required = True,
+        source = contextual_reg_num_source,
+        )
+
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = False,
+        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
+        show_year = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+
+    special_application = schema.Choice(
+        title = _(u'Special Application'),
+        source = SpecialApplicationSource(),
+        required = True,
+        )
+
+class IApplicantEdit(IApplicant):
+    """This is an applicant interface for editing.
+
+    Here we can repeat the fields from base data and set the
+    `required` and `readonly` attributes to True to further restrict
+    the data access. Or we can allow only certain certificates to be
+    selected by choosing the appropriate source.
+
+    We cannot omit fields here. This has to be done in the
+    respective form page.
+    """
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+    course1 = schema.Choice(
+        title = _(u'1st Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = True,
+        )
+
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        required = False,
+        readonly = True,
+        )
+
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        readonly = True,
+        )
+
+IApplicantEdit['email'].order = IApplicantEdit['sex'].order
+
+class IApplicantUpdateByRegNo(IApplicant):
+    """Skip regular reg_number validation if reg_number is used for finding
+    the applicant object.
+    """
+    reg_number = schema.TextLine(
+        title = u'Registration Number',
+        required = False,
+        )
+
+class IApplicantRegisterUpdate(IApplicant):
+    """This is a representation of an applicant for first-time registration.
+    This interface is used when applicants use the registration page to
+    update their records.
+    """
+    reg_number = schema.TextLine(
+        title = u'Registration Number',
+        required = True,
+        )
+
+    #firstname = schema.TextLine(
+    #    title = _(u'First Name'),
+    #    required = True,
+    #    )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+class IApplicantOnlinePayment(IOnlinePayment):
+    """An applicant payment via payment gateways.
+    """
+
+    def doAfterApplicantPayment():
+        """Process applicant after payment was made.
+        """
+
+    def doAfterApplicantPaymentApproval():
+        """Process applicant after payment was approved.
+        """
+
+    def approveApplicantPayment():
+        """Approve payment and process applicant.
+        """
+
+class IApplicantRefereeReport(IKofaObject):
+    """A referee report.
+    """
+
+    r_id = Attribute('Report identifier')
+
+    creation_date = schema.Datetime(
+        title = _(u'Ticket Creation Date'),
+        readonly = False,
+        required = False,
+        )
+
+    name = schema.TextLine(
+        title = _(u'Name'),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+
+    report = schema.Text(
+        title = _(u'Report'),
+        required = False,
+        )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/payment.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/payment.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/payment.py	(revision 16045)
@@ -0,0 +1,153 @@
+## $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
+##
+"""
+Application fee payment.
+"""
+import grok
+import sys
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, InvalidTransitionError)
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.payments import OnlinePayment
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.applicants.interfaces import IApplicantOnlinePayment
+from waeup.kofa.applicants.workflow import PAID
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class ApplicantOnlinePayment(OnlinePayment):
+    """This is an online payment.
+    """
+    grok.implements(IApplicantOnlinePayment)
+    grok.provides(IApplicantOnlinePayment)
+
+    def __init__(self):
+        super(ApplicantOnlinePayment, self).__init__()
+        return
+
+    def doAfterApplicantPaymentApproval(self):
+        """Process applicant after payment was approved.
+        """
+        if self.__parent__.state != PAID \
+            and (self.p_category == 'application' or self.__parent__.special):
+            wf_info = IWorkflowInfo(self.__parent__)
+            try:
+                wf_info.fireTransition('approve')
+            except InvalidTransitionError:
+                msg = log = 'Error: %s' % sys.exc_info()[1]
+                return 'danger', msg, log
+        log = 'approved: %s' % self.p_id
+        msg = _('Payment approved')
+        flashtype = 'success'
+        return flashtype, msg, log
+
+    def doAfterApplicantPayment(self):
+        """Process applicant after payment was made.
+        """
+        if self.__parent__.state != PAID \
+            and (self.p_category == 'application' or self.__parent__.special):
+            wf_info = IWorkflowInfo(self.__parent__)
+            try:
+                wf_info.fireTransition('pay')
+            except InvalidTransitionError:
+                msg = log = 'Error: %s' % sys.exc_info()[1]
+                return 'danger', msg, log
+        log = 'successful payment: %s' % self.p_id
+        msg = _('Payment successfully completed. Kindly submit application for processing.')
+        flashtype = 'success'
+        return flashtype, msg, log
+
+    def approveApplicantPayment(self):
+        """Approve payment and process applicant.
+        """
+        if self.p_state == 'paid':
+            return 'warning', _('This ticket has already been paid.'), None
+        self.approve()
+        return self.doAfterApplicantPaymentApproval()
+
+ApplicantOnlinePayment = attrs_to_fields(
+    ApplicantOnlinePayment, omit=['display_item'])
+
+class Payer(grok.Adapter):
+    """An adapter to publish applicant data through a simple webservice.
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.implements(IPayer)
+
+    @property
+    def display_fullname(self):
+        "Name of  payer"
+        return self.context.__parent__.display_fullname
+
+    @property
+    def id(self):
+        "Id of payer"
+        return self.context.__parent__.applicant_id
+
+    @property
+    def reg_number(self):
+        "Reg number of payer"
+        return self.context.__parent__.reg_number
+
+    @property
+    def matric_number(self):
+        return 'N/A'
+
+    @property
+    def faculty(self):
+        return 'N/A'
+
+    @property
+    def department(self):
+        return 'N/A'
+
+    @property
+    def email(self):
+        "Email of payer"
+        return self.context.__parent__.email
+
+    @property
+    def phone(self):
+        "Phone number of payer"
+        return self.context.__parent__.phone
+
+    @property
+    def current_mode(self):
+        "Current study mode of payer"
+        return 'N/A'
+
+    @property
+    def current_level(self):
+        "Current level of payer"
+        return 'N/A'
+
+# Applicant online payments must be importable. So we might need a factory.
+class ApplicantOnlinePaymentFactory(grok.GlobalUtility):
+    """A factory for applicant online payments.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.ApplicantOnlinePayment')
+    title = u"Create a new online payment.",
+    description = u"This factory instantiates new online payment instances."
+
+    def __call__(self, *args, **kw):
+        return ApplicantOnlinePayment()
+
+    def getInterfaces(self):
+        return implementedBy(ApplicantOnlinePayment)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/pdf.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/pdf.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/pdf.py	(revision 16045)
@@ -0,0 +1,226 @@
+## $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
+##
+"""
+Generate PDF docs for applicants.
+"""
+import grok
+from reportlab.platypus import Paragraph, Spacer, Table
+from reportlab.lib.units import cm
+from zope.component import getUtility
+from zope.i18n import translate
+from waeup.kofa.applicants.interfaces import IApplicant, IApplicantsUtils
+from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
+from waeup.kofa.browser.interfaces import IPDFCreator
+from waeup.kofa.browser.pdf import SMALL_PARA_STYLE, ENTRY1_STYLE, get_qrcode
+from waeup.kofa.interfaces import IExtFileStore, IPDF, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.widgets.datewidget import FriendlyDateDisplayWidget
+
+SLIP_STYLE = [
+    ('VALIGN',(0,0),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
+    ]
+
+class PDFApplicationSlip(grok.Adapter):
+    """Create a PDF application slip for applicants.
+    """
+    # XXX: Many things in here are reusable. We might want to split
+    # things. Possibly move parts to IPDFCreator?
+    grok.context(IApplicant)
+    grok.implements(IPDF)
+    grok.name('application_slip')
+
+    form_fields =  grok.AutoFields(IApplicant).omit(
+        'locked', 'course_admitted', 'suspended',
+        )
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    column_two_fields = ('applicant_id', 'reg_number',
+        'firstname', 'middlename', 'lastname')
+    two_columns_design_fields = []
+
+    @property
+    def note(self):
+        note = getattr(self.context.__parent__, 'application_slip_notice', None)
+        if note:
+            return '<br /><br />' + note
+        return
+
+    @property
+    def target(self):
+        return getattr(self.context.__parent__, 'prefix', None)
+
+    @property
+    def title(self):
+        container_title = self.context.__parent__.title
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        ar_translation = translate(_('Application Record'),
+            'waeup.kofa', target_language=portal_language)
+        return '%s - %s %s' % (container_title,
+            ar_translation, self.context.application_number)
+
+    def _getCourseAdmittedLink(self, view):
+        """Return link, title and code in html format to the certificate
+           admitted.
+        """
+        course_admitted = self.context.course_admitted
+        if view is not None and getattr(course_admitted, '__parent__',None):
+            url = view.url(course_admitted)
+            title = course_admitted.title
+            code = course_admitted.code
+            return '<a href="%s">%s - %s</a>' %(url,code,title)
+        return ''
+
+    def _getDeptAndFaculty(self):
+        """Return long titles of department and faculty.
+
+        Returns a list [department_title, faculty_title]
+
+        If the context applicant has no course admitted or dept. and
+        faculty cannot be determined, ``None`` is returned.
+        """
+        course_admitted = self.context.course_admitted
+        dept = getattr(
+                getattr(course_admitted, '__parent__', None),
+                '__parent__', None)
+        faculty = getattr(dept, '__parent__', None)
+        return [x is not None and x.longtitle or x for x in dept, faculty]
+
+    def _addLoginInformation(self):
+        if self.context.state == 'created':
+            comment = translate(_(
+                '\n Proceed to the login page of the portal' +
+                ' and enter your new credentials:' +
+                ' user name= ${a}, password = ${b}. ' +
+                'Change your password when you have logged in.',
+                mapping = {
+                    'a':self.context.student_id,
+                    'b':self.context.application_number}
+                ))
+            return comment
+        return
+
+    def _getPDFCreator(self):
+        return getUtility(IPDFCreator)
+
+    def __call__(self, view=None, note=None):
+        """Return a PDF representation of the context applicant.
+
+        If no `view` is given, the course admitted field will be an
+        empty string and author will be set as ``'unknown'``.
+
+        If a `view` is given, author will be set as the calling
+        principal.
+        """
+        doc_title = '\n'.join([x.strip() for x in self.title.split(' - ')])
+        data = []
+        topMargin = 1.5
+        if len(self.title.split(' - ')) > 2:
+            topMargin = 1.8
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        separators = getUtility(IApplicantsUtils).SEPARATORS_DICT
+        creator = self._getPDFCreator()
+
+        # append history
+        data.extend(creator.fromStringList(self.context.history.messages))
+        data.append(Spacer(1, 5))
+
+        if self.context.__parent__.with_picture:
+
+            # create three-column header table
+            # append photograph to the left
+            img_path = getattr(
+                getUtility(IExtFileStore).getFileByContext(self.context),
+                'name', DEFAULT_PASSPORT_IMAGE_PATH)
+            data_left = [[creator.getImage(img_path)]]
+            table_left = Table(data_left,style=SLIP_STYLE)
+
+            # append base data table to the middle
+            fields = [
+                field for field in self.form_fields
+                    if field.__name__ in self.column_two_fields]
+            table_middle = creator.getWidgetsTable(
+                fields, self.context, None, lang=portal_language,
+                separators=None, colWidths=[5*cm, 4.5*cm])
+
+            # append QR code to the right
+            if view is not None:
+                url = view.url(self.context, 'application_slip.pdf')
+                data_right = [[get_qrcode(url, width=70.0)]]
+                table_right = Table(data_right,style=SLIP_STYLE)
+            else:
+                table_right = None
+
+            header_table = Table(
+                [[table_left, table_middle, table_right],],style=SLIP_STYLE)
+            data.append(header_table)
+
+        else:
+            fields = [
+                field for field in self.form_fields
+                    if field.__name__ in self.column_two_fields]
+            data.append(creator.getWidgetsTable(
+                fields, self.context, None, lang=portal_language,
+                separators=None, colWidths=[7*cm, 10*cm]))
+
+        # append widgets except those already added in column two
+        # and those in two_columns_design_fields
+        dept, faculty = self._getDeptAndFaculty()
+        fields = [
+            field for field in self.form_fields
+                if not field.__name__ in self.column_two_fields and
+                not field.__name__ in self.two_columns_design_fields]
+        data.append(creator.getWidgetsTable(
+            fields, self.context, view, lang=portal_language,
+            domain='waeup.kofa', separators=separators,
+            course_label='Admitted Course of Study:',
+            course_link=self._getCourseAdmittedLink(view),
+            dept=dept, faculty=faculty, colWidths=[7*cm, 10*cm]))
+
+        # append two-column table of widgets of those fields defined in
+        # two_columns_design_fields
+        if len(self.two_columns_design_fields):
+            fields = [
+                field for field in self.form_fields
+                    if not field.__name__ in self.column_two_fields and
+                    field.__name__ in self.two_columns_design_fields]
+            if fields:
+                data.append(creator.getWidgetsTable(
+                    fields, self.context, view, lang=portal_language,
+                    domain='waeup.kofa', separators=separators,
+                    twoDataCols=True))
+
+        # append QR Code if without picture
+        if not self.context.__parent__.with_picture and view is not None:
+            data.append(Spacer(1, 5))
+            url = view.url(self.context, 'application_slip.pdf')
+            data_right = [[get_qrcode(url, width=70.0)]]
+            table_right = Table(data_right,style=SLIP_STYLE)
+            data.append(get_qrcode(url, width=70.0))
+
+        # append login information
+        note = None
+        login_information = self._addLoginInformation()
+        if not None in (self.note, login_information):
+            note = self.note + login_information
+        elif self.note:
+            note = self.note
+        elif login_information:
+            note = login_information
+
+        return creator.create_pdf(data, None, doc_title, note=note,
+            topMargin=topMargin)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/permissions.py	(revision 16045)
@@ -0,0 +1,114 @@
+## $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
+##
+"""
+Local permissions for applicants/applications.
+"""
+import grok
+
+# Application permissions
+
+class HandleApplication(grok.Permission):
+    """The HandleApplication permission is reserved for applicants.
+    Applicants 'handle' their data. Officers 'manage' the data.
+    """
+    grok.name('waeup.handleApplication')
+
+class ViewApplication(grok.Permission):
+    """The ViewApplication permission allows to view application records.
+    """
+    grok.name('waeup.viewApplication')
+
+class ViewApplicationsTab(grok.Permission):
+    grok.name('waeup.viewApplicantsTab')
+
+class ViewMyApplicationDataTab(grok.Permission):
+    grok.name('waeup.viewMyApplicationDataTab')
+
+class ManageApplication(grok.Permission):
+    """The ManageApplication permission allows to edit the data. This
+    permission is reserved for officers and portal managers.
+    """
+    grok.name('waeup.manageApplication')
+
+class ViewApplicationStatistics(grok.Permission):
+    """The ViewApplicationStatistics permission allows to perform statistical
+    evaluations.
+    """
+    grok.name('waeup.viewApplicationStatistics')
+
+class PayApplicant(grok.Permission):
+    """The PayApplicant permission allows to add an online payment ticket.
+    """
+    grok.name('waeup.payApplicant')
+
+class CreateStudents(grok.Permission):
+    """The CreateStudents permission allows to create a bunch student
+    records from application records.
+    """
+    grok.name('waeup.createStudents')
+
+# Local role
+
+class ApplicationOwner(grok.Role):
+    """An applicant 'owns' her/his application record and
+    gains permissions to handle the record, upload a passport picture or
+    add payment tickets.
+    """
+    grok.name('waeup.local.ApplicationOwner')
+    grok.title(u'Application Owner')
+    grok.permissions('waeup.handleApplication',
+                     'waeup.viewApplication',
+                     'waeup.payApplicant')
+
+# Site roles
+
+class ApplicantRole(grok.Role):
+    """This role is dedicated to applicants only. It defines the permissions
+    an applicant gains portal-wide.
+    """
+    grok.name('waeup.Applicant')
+    grok.title(u'Applicant (do not assign)')
+    grok.permissions('waeup.viewAcademics', 'waeup.viewMyApplicationDataTab',
+                     'waeup.Authenticated')
+
+class ApplicationsOfficer(grok.Role):
+    """The Applications Officer is allowed to view all application records.
+    """
+    grok.name('waeup.ApplicationsOfficer')
+    grok.title(u'Applications Officer (view only)')
+    grok.permissions('waeup.viewApplication', 'waeup.viewApplicantsTab')
+
+class ApplicationsManager(grok.Role):
+    """The Applications Manager is allowed to edit all application records.
+    The role also allows to add payment tickets and view statistics.
+    """
+    grok.name('waeup.ApplicationsManager')
+    grok.title(u'Applications Manager')
+    grok.permissions('waeup.manageApplication', 'waeup.viewApplication',
+                     'waeup.viewApplicantsTab', 'waeup.payApplicant',
+                     'waeup.viewApplicationStatistics')
+
+class StudentsCreator(grok.Role):
+    """The Students Creator is allowed to create a bunch of student 
+    records from application records.
+    """
+    grok.name('waeup.StudentsCreator')
+    grok.title(u'Students Creator')
+    grok.permissions('waeup.viewApplication',
+                     'waeup.viewApplicantsTab',
+                     'waeup.createStudents')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/refereereport.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/refereereport.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/refereereport.py	(revision 16045)
@@ -0,0 +1,61 @@
+## $Id$
+##
+## Copyright (C) 2016 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
+##
+import os
+import grok
+from datetime import datetime
+from zope.component import getUtility, createObject, getAdapter
+from zope.component.interfaces import IFactory
+from zope.event import notify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.interface import implementedBy
+from zope.schema.interfaces import RequiredMissing, ConstraintNotSatisfied
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.applicants.interfaces import IApplicantRefereeReport
+
+
+class ApplicantRefereeReport(grok.Model):
+    """This is referee report.
+    """
+    grok.implements(IApplicantRefereeReport)
+    grok.provides(IApplicantRefereeReport)
+
+    def __init__(self):
+        super(ApplicantRefereeReport, self).__init__()
+        self.r_id = None
+        self.creation_date = datetime.utcnow()
+        return
+
+ApplicantRefereeReport = attrs_to_fields(ApplicantRefereeReport)
+
+
+class ApplicantRefereeReportFactory(grok.GlobalUtility):
+    """A factory for applicant online payments.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.ApplicantRefereeReport')
+    title = u"Create a new referee report.",
+    description = u"This factory instantiates new referee report instances."
+
+    def __call__(self, *args, **kw):
+        return ApplicantRefereeReport()
+
+    def getInterfaces(self):
+        return implementedBy(ApplicantRefereeReport)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/root.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/root.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/root.py	(revision 16045)
@@ -0,0 +1,180 @@
+## $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
+##
+"""
+The root for applicants.
+"""
+import grok
+from hurry.query import Eq
+from hurry.query.interfaces import IQuery
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.component import getUtility
+from zope.component.interfaces import ComponentLookupError
+from zope.catalog.interfaces import ICatalog
+from zope.catalog.field import FieldIndex
+from zope.schema import getFields
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.applicants.interfaces import IApplicantsRoot
+from waeup.kofa.utils.logger import Logger
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class ApplicantsRoot(grok.Container, Logger):
+    """The root of applicants-related components. It contains only
+    containers for applicants.
+    """
+    grok.implements(IApplicantsRoot)
+
+    local_roles = []
+    description_dict = {}
+    logger_name = 'waeup.kofa.${sitename}.applicants'
+    logger_filename = 'applicants.log'
+
+ApplicantsRoot = attrs_to_fields(ApplicantsRoot)
+
+class ApplicantsPlugin(grok.GlobalUtility):
+    """A KofaPlugin that creates an applicants root in portal.
+
+    This plugin could be called by a typical
+    `waeup.kofa.app.Universtiy` instance on creation time. The
+    :meth:`update` method normally can also be triggered manually over
+    the main site configuration.
+
+    Implements :class:`waeup.kofa.interfaces.IKofaPluggable`
+    """
+    grok.name('applicants')
+    grok.implements(IKofaPluggable)
+    log_prefix = 'ApplicantsPlugin'
+
+    def setup(self, site, name, logger):
+        """Create a new :class:`ApplicantsRoot` instance in `site`.
+        """
+        site['applicants'] = ApplicantsRoot()
+        logger.info(
+            '%s: Installed applicants root.' % (self.log_prefix,)
+            )
+        return
+
+    def update(self, site, name, logger):
+        """Update site wide ``applicants`` root.
+
+        If the site already contains a suitable ``applicants`` root,
+        leave it that way. If not create one and delete the old one if
+        appropriate.
+
+        Update applicants catalog.
+
+        Remove deprecated applicant ids from global role map.
+        """
+        app_folder = site.get('applicants', None)
+        site_name = getattr(site, '__name__', '<Unnamed Site>')
+        if IApplicantsRoot.providedBy(app_folder):
+            items = getFields(IApplicantsRoot).items()
+            nothing_to_do = True
+            #for i in items:
+            #    if not hasattr(app_folder,i[0]):
+            #        nothing_to_do = False
+            #        setattr(app_folder,i[0],i[1].missing_value)
+            #        logger.info(
+            #            '%s: %s added to root.' % (self.log_prefix,i[0]))
+            # can be removed after upgrading futminna
+            #if not hasattr(app_folder, 'description_dict'):
+            #    nothing_to_do = False
+            #    setattr(app_folder,'description_dict',{})
+            #    logger.info(
+            #        '%s: description_dict added to root.' % self.log_prefix)
+
+            # Add container_code index
+            try:
+                cat = getUtility(ICatalog, name='applicants_catalog')
+                if 'container_code' not in cat.keys():
+                    nothing_to_do = False
+                    cat[u'container_code'] = FieldIndex(field_name=u'container_code')
+                    cat.updateIndexes()
+                    logger.info(
+                        '%s: container_code index added to applicants_catalog.'
+                        % self.log_prefix)
+            except ComponentLookupError: # in unit tests
+                pass
+
+            # Remove old applicant ids from global role map.
+            try:
+                removed_applicant_ids = []
+                cat = getUtility(ICatalog, name='applicants_catalog')
+                role_manager = IPrincipalRoleManager(grok.getSite())
+                principals = role_manager.getPrincipalsForRole('waeup.Applicant')
+                for principal in principals:
+                    applicant_id = principal[0]
+                    results = cat.searchResults(applicant_id=(applicant_id, applicant_id))
+                    if len(results):
+                        continue
+                    # The applicant does no longer exist.
+                    nothing_to_do = False
+                    role_manager.unsetRoleForPrincipal(
+                        'waeup.Applicant', applicant_id)
+                    removed_applicant_ids.append(applicant_id)
+                if len(removed_applicant_ids):
+                    logger.info(
+                        '%s: %s applicant ids removed from global role map: %s'
+                        % (self.log_prefix, len(removed_applicant_ids),
+                           removed_applicant_ids))
+            except ComponentLookupError: # in unit tests
+                pass
+
+            # Remove old student ids from global role map.
+            try:
+                removed_student_ids = []
+                cat = getUtility(ICatalog, name='students_catalog')
+                role_manager = IPrincipalRoleManager(grok.getSite())
+                principals = role_manager.getPrincipalsForRole('waeup.Student')
+                for principal in principals:
+                    student_id = principal[0]
+                    results = cat.searchResults(student_id=(student_id, student_id))
+                    if len(results):
+                        continue
+                    # The student does no longer exist.
+                    nothing_to_do = False
+                    role_manager.unsetRoleForPrincipal(
+                        'waeup.Student', student_id)
+                    removed_student_ids.append(student_id)
+                if len(removed_applicant_ids):
+                    logger.info(
+                        '%s: %s student ids removed from global role map: %s'
+                        % (self.log_prefix, len(removed_student_ids),
+                           removed_student_ids))
+            except ComponentLookupError: # in unit tests
+                pass
+
+            if nothing_to_do:
+                logger.info(
+                    '%s: Updating site at %s: Nothing to do.' % (
+                        self.log_prefix, site_name,)
+                    )
+            return
+        elif app_folder is not None:
+            # Applicants need update. Remove old instance.
+            logger.warn(
+                '%s: Outdated applicants folder detected at site %s.'
+                'Removing it.' % (self.log_prefix, site_name)
+                    )
+            del site['applicants']
+        # Add new applicants.
+        logger.info(
+            '%s: Updating site at %s. Installing '
+            'applicants.' % (self.log_prefix, site_name,)
+            )
+        self.setup(site, name, logger)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data.csv	(revision 16045)
@@ -0,0 +1,5 @@
+application_number,container_code,reg_number,firstname,lastname,date_of_birth,email,course1,sex,middlename,password
+1234,dp2011,1001,Aaren,Pieri,1990-01-02,xx@yy.zz,CERT1,m,Peter,mypwd1
+2345,dp2011,1002,Aaren,Finau,1990-01-03,xx@yy.zz,CERT1,m,Claus,
+,dp2011,1003,Aaren,Berson,1990-01-04,xx@yy.zz,CERT1,m,Alfons,
+4567,dp2011,1004,Simon,Star,1990-01-05,xx@yy.zz,CERT1,m,Uwe,{SSHA}21fUeiWxLAErnTG7FZoXwf5HuKIdWAyS
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv	(revision 16045)
@@ -0,0 +1,4 @@
+applicant_id,reg_number,firstname,middlename,state
+,1001,Aaren,,
+dp2011_2345,6666,Alfons,,admitted
+,1003,Abraham,,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv	(revision 16045)
@@ -0,0 +1,5 @@
+applicant_id,reg_number,firstname,middlename,state
+dp2011_1234,,Aaren,XXX,
+,6666,Alfons,XXX,
+,1003,Abraham,XXX,
+,1004,Vertan,,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_container_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_container_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_container_data.csv	(revision 16045)
@@ -0,0 +1,7 @@
+code,title,prefix,year,application_category,description,startdate,enddate,strict_deadline,mode
+app2017,General Studies,app,2017,basic,"This text can been seen by anonymous users.
+>>de<<
+Dieser Text kann von anonymen Benutzern gelesen werden.",2012-03-01,2012-04-25,1,create
+app2018,General Studies,app,2018,basic,"This text can been seen by anonymous users.
+>>de<<
+Dieser Text kann von anonymen Benutzern gelesen werden.",2013-03-01,2013-04-25,1,create
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv	(revision 16045)
@@ -0,0 +1,6 @@
+application_number,container_code,reg_number,firstname,lastname,date_of_birth,email,course1,sex,middlename,password
+,dp2011,1001,Aaren,Pieri,01/02/1990,xx@yy.zz,CERT1,m,Claus,
+5678,dp2011,1005,Jim,Knopf,1990-01-06,xx@yy.zz,CERT2,m,,
+6789,dp2011,1006,Lukas,Knopf,1990-01-07,xx@yy.zz,CERT3,m,,
+,,8888,Mister,No,1990-01-07,no@yy.zz,CERT1,m,,
+,dp2012,9999,Miss,Nobody,1990-01-07,nobody@yy.zz,CERT1,m,,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_payment_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_payment_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/sample_payment_data.csv	(revision 16045)
@@ -0,0 +1,3 @@
+applicant_id,p_state,p_category,p_id,p_session,r_amount_approved,amount_auth,creation_date,type
+dp2011_1234,failed,application,,2016,19500.1,10500.1,2011-11-25 20:16:33.757 WAT,online
+dp2011_1234,paid,application,p1266236341955,2015,19500.1,19500.1,2010-11-25 20:16:33.757 WAT,online
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicant.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicant.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicant.py	(revision 16045)
@@ -0,0 +1,253 @@
+## $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
+##
+"""Tests for applicants and related.
+"""
+import grok
+import shutil
+import tempfile
+from StringIO import StringIO
+from zope.component import adapts, queryUtility
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from waeup.kofa.image.interfaces import IKofaImageFile
+from waeup.kofa.imagestorage import DefaultStorage
+from waeup.kofa.interfaces import IFileStoreHandler, IFileStoreNameChooser
+from waeup.kofa.applicants import (
+    Applicant, ApplicantFactory,
+    ApplicantImageStoreHandler, ApplicantImageNameChooser,
+    )
+from waeup.kofa.applicants.interfaces import IApplicant
+from waeup.kofa.applicants.payment import ApplicantOnlinePayment
+from waeup.kofa.applicants.refereereport import ApplicantRefereeReport
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+
+class FakeImageLocation(object):
+    implements(ILocation)
+    adapts(IKofaImageFile)
+    def __init__(self, context):
+        pass
+
+class HelperTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(HelperTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(HelperTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_image_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for applicants
+        handler = queryUtility(IFileStoreHandler, name='img-applicant')
+        self.assertTrue(
+            isinstance(handler, ApplicantImageStoreHandler))
+        return
+
+    def test_image_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='img-applicant')
+        result1 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__sample.jpg')
+        result2 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__dir1/sample.jpg')
+        result3 = handler.pathFromFileID(
+            store, '/fake-root', '__img-applicant__dir1/dir2/sample.jpg')
+        self.assertEqual(
+            result1, '/fake-root/applicants/sample')
+        self.assertEqual(
+            result2, '/fake-root/applicants/dir1/sample')
+        self.assertEqual(
+            result3, '/fake-root/applicants/dir1/dir2/sample')
+        return
+
+    def test_image_store_handler_create(self):
+        # we can create files in image store with the applicant image
+        # store handler
+        store = DefaultStorage(self.workdir)
+        handler = queryUtility(IFileStoreHandler, name='img-applicant')
+        file_obj, path, waeup_file = handler.createFile(
+            store, store.root, 'sample.jpg', '__img_applicant__sample',
+            StringIO('I am a JPG file'))
+        self.assertEqual(path[-21:], 'applicants/sample.jpg')
+        return
+
+    #def test_image_store_handler_invalid_filename_ext(self):
+    #    # we only accept '.jpg' and '.png' as filename extensions.
+    #    store = DefaultStorage(self.workdir)
+    #    handler = queryUtility(IFileStoreHandler, name='img-applicant')
+    #    self.assertRaises(
+    #        ValueError,
+    #        handler.createFile,
+    #        store, store.root, 'sample.txt', '__img_applicant__sample',
+    #        StringIO('I am a txt file'))
+    #    return
+
+class ApplicantImageNameChooserTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = ApplicantImageNameChooser(None) # needs a context normally
+        verify.verifyClass(IFileStoreNameChooser, ApplicantImageNameChooser)
+        verify.verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for applicant objects as adapter
+        appl = Applicant()
+        chooser = IFileStoreNameChooser(appl)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_applicant_wo_container(self):
+        # we can get an image filename for applicants not in a container
+        appl = Applicant()
+        appl.applicant_id = u'dummy_123456'
+        chooser = IFileStoreNameChooser(appl)
+        result = chooser.chooseName()
+        # the file would be stored in a ``_default`` directory.
+        self.assertEqual(
+            result, '__img-applicant___default/dummy_123456.jpg')
+        return
+
+    def test_name_chooser_applicant_w_container(self):
+        fake_container = grok.Container()
+        fake_container.__name__ = 'folder'
+        fake_container.code = 'folder'
+        appl = Applicant()
+        appl.applicant_id = u'folder_123456'
+        appl.__parent__ = fake_container
+        chooser = IFileStoreNameChooser(appl)
+        result = chooser.chooseName()
+        self.assertEqual(
+            result, '__img-applicant__folder/%s.jpg' % appl.applicant_id)
+        return
+
+    def test_name_chooser_applicant_nonpassport_w_container(self):
+        fake_container = grok.Container()
+        fake_container.__name__ = 'folder'
+        fake_container.code = 'folder'
+        appl = Applicant()
+        appl.applicant_id = u'folder_123456'
+        appl.__parent__ = fake_container
+        chooser = IFileStoreNameChooser(appl)
+        result = chooser.chooseName(attr='anything.pdf')
+        self.assertEqual(
+            result, '__img-applicant__folder/anything_%s.pdf' % appl.applicant_id)
+        return
+
+    def test_name_chooser_check_name(self):
+        # we can check file ids for applicants
+        fake_container = grok.Container()
+        fake_container.__name__ = 'folder'
+        fake_container.code = 'folder'
+        appl = Applicant()
+        appl.applicant_id = u'folder_123456'
+        appl.__parent__ = fake_container
+        chooser = IFileStoreNameChooser(appl)
+        result1 = chooser.checkName('foo')
+        result2 = chooser.checkName(
+            '__img-applicant__folder/%s.jpg' % appl.applicant_id)
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, True)
+        return
+
+class ApplicantTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantTest, self).setUp()
+        self.applicant = Applicant()
+        return
+
+    def tearDown(self):
+        super(ApplicantTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IApplicant, Applicant)
+        verify.verifyObject(IApplicant, self.applicant)
+        return
+
+    def test_container_code(self):
+        fake_container = grok.Container()
+        fake_container.__name__ = 'folder'
+        fake_container.code = 'folder'
+        appl = Applicant()
+        appl.__parent__ = fake_container
+        self.assertTrue(appl.password is None)
+        self.assertTrue(appl.firstname is None)
+        self.assertTrue(appl.lastname is None)
+        self.assertTrue(appl.email is None)
+        self.assertEqual(appl.container_code, 'folder-')
+        appl.firstname = u'Anna'
+        self.assertEqual(appl.container_code, 'folder+')
+        return
+
+    def test_payments(self):
+        payment = ApplicantOnlinePayment()
+        no_payment = ApplicantRefereeReport()
+        self.applicant['pid'] = payment
+        self.applicant['nopid'] = no_payment
+        self.assertEqual(len(self.applicant.values()),2)
+        self.assertEqual(len(self.applicant.payments),1)
+        self.assertEqual(self.applicant.payments[0], payment)
+        return
+
+    def test_refereereports(self):
+        report = ApplicantRefereeReport()
+        no_report = ApplicantOnlinePayment()
+        self.applicant['rid'] = report
+        self.applicant['norid'] = no_report
+        self.assertEqual(len(self.applicant.values()),2)
+        self.assertEqual(len(self.applicant.refereereports),1)
+        self.assertEqual(self.applicant.refereereports[0], report)
+        return
+
+class ApplicantFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantFactoryTest, self).setUp()
+        self.factory = ApplicantFactory()
+        return
+
+    def tearDown(self):
+        super(ApplicantFactoryTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, ApplicantFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, Applicant)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IApplicant)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicantcopier.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicantcopier.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_applicantcopier.py	(revision 16045)
@@ -0,0 +1,286 @@
+## $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
+##
+"""
+Tests for the creation of student containers with data from admitted applicants.
+"""
+import os
+import grok
+from StringIO import StringIO
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.component import getUtility
+from zope.i18n import translate
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+
+session = datetime.now().year - 2
+
+class ApplicantCopierFunctionalTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantCopierFunctionalTests, self).setUp()
+        return
+
+    def prepare_applicant(self):
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        # Upload a passport picture
+        ctrl = self.browser.getControl(name='form.passport')
+        file_obj = open(
+            os.path.join(os.path.dirname(__file__), 'test_image.jpg'),'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
+        self.browser.getControl("Save").click() # submit form
+        return
+
+    def test_copier(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.prepare_applicant()
+        storage = getUtility(IExtFileStore)
+        # The stored image can be fetched
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        fd = storage.getFile(file_id)
+        file_len_orig = len(fd.read())
+        # We store a test pdf file
+        dummy_file = StringIO('test file')
+        testfile_id = IFileStoreNameChooser(
+            self.applicant).chooseName(attr='testfile.pdf')
+        test_file = storage.createFile(testfile_id, dummy_file)
+        # The stored file can be fetched
+        fd = storage.getFile(testfile_id)
+        testfile_len_orig = len(fd.read())
+        self.assertEqual(self.app['students']._curr_stud_id, 1000000)
+        # Let's try to create the student
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue(msg == 'Applicant has not yet been admitted.')
+        IWorkflowState(self.applicant).setState('admitted')
+        (success, msg) = self.applicant.createStudent()
+        # Current student_id has not changed.
+        self.assertEqual(self.app['students']._curr_stud_id, 1000000)
+        self.assertTrue(msg == 'No course admitted provided.')
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        # Maybe the certificate has meanwhile been removed
+        del self.app['faculties']['fac1']['dep1'].certificates['CERT1']
+        (success, msg) = self.applicant.createStudent()
+        self.assertFalse(success)
+        self.assertTrue('ConstraintNotSatisfied: CERT1' in msg)
+        # Current student_id has not changed.
+        self.assertEqual(self.app['students']._curr_stud_id, 1000000)
+        # Ok, lets add the certificate and try again
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        # Managers are not allowed to trigger the create transition manually
+        self.assertFalse('<option value="create">' in self.browser.contents)
+        # Student can be created
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue('created' in msg)
+        # Current student_id has not changed.
+        self.assertEqual(self.app['students']._curr_stud_id, 1000001)
+        # The applicant is locked
+        self.assertTrue(self.applicant.locked)
+        student_id = translate(msg, 'waeup.kofa').split()[1]
+        # View student container just created
+        student = self.app['students'][student_id]
+        student_path = 'http://localhost/app/students/%s' % student_id
+        self.browser.open(student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, student_path)
+        # Student is in state admitted
+        self.assertEqual(student.state, 'admitted')
+        # Attributes properly set?
+        self.assertEqual(student.email, 'xx@yy.zz')
+        self.assertEqual(student.firstname, 'John')
+        self.assertEqual(student.middlename, 'Anthony')
+        self.assertEqual(student.lastname, 'Tester')
+        # student_id set in application record?
+        self.assertEqual(self.applicant.student_id, student.student_id)
+        # Check if passport image has been copied
+        file_id = IFileStoreNameChooser(student).chooseName(attr='passport.jpg')
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len_orig, file_len)
+        # Check if test file has been copied too (new)
+        file_id = IFileStoreNameChooser(student).chooseName(attr='testfile.jpg')
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(testfile_len_orig, file_len)
+        # Check if application slip exists and is a PDF file
+        file_id = IFileStoreNameChooser(
+            student).chooseName(attr='application_slip.pdf')
+        pdf = storage.getFile(file_id).read()
+        self.assertTrue(len(pdf) > 0)
+        self.assertEqual(pdf[:8], '%PDF-1.4')
+        path = os.path.join(samples_dir(), 'application_slip.pdf')
+        open(path, 'wb').write(pdf)
+        print "Sample PDF application_slip.pdf written to %s" % path
+        # Check if there is an application slip link in UI
+        self.assertTrue('Application Slip' in self.browser.contents)
+        self.browser.getLink("Application Slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        # Has the student been properly indexed?
+        # Yes, we can find the student in department
+        self.browser.open('http://localhost/app/students')
+        self.browser.getControl(name="searchtype").value = ['depcode']
+        self.browser.getControl(name="searchterm").value = 'dep1'
+        self.browser.getControl("Find student(s)").click()
+        self.assertMatches('...John Anthony Tester...', self.browser.contents)
+        # Has the student studycourse the correct attributes?
+        self.assertEqual(student['studycourse'].certificate.code, 'CERT1')
+        self.assertEqual(student['studycourse'].entry_session, session)
+        self.assertEqual(student['studycourse'].entry_mode, 'ug_ft')
+        self.assertEqual(student['studycourse'].current_session, session)
+        self.assertEqual(student['studycourse'].current_level, 100)
+
+    def test_batch_copying(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.prepare_applicant()
+        IWorkflowState(self.applicant).setState('admitted')
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl("Create students from selected", index=0).click()
+        self.assertTrue('No applicant selected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=self.applicant.application_number).selected = True
+        self.browser.getControl("Create students from selected", index=0).click()
+        self.assertTrue('1 students successfully created' in self.browser.contents)
+
+    def test_hidden_batch_copying_container(self):
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.prepare_applicant()
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue('No record found' in self.browser.contents)
+        self.assertFalse('Failed records' in self.browser.contents)
+        self.assertFalse('Successfully created records' in self.browser.contents)
+        IWorkflowState(self.applicant).setState('admitted')
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue('No course admitted provided' in self.browser.contents)
+        self.assertFalse('Successfully created records' in self.browser.contents)
+        self.assertTrue('Failed records' in self.browser.contents)
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        # date_of_birth is not required for applicants but for students
+        self.applicant.date_of_birth = None
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue('RequiredMissing: date_of_birth' in self.browser.contents)
+        self.assertTrue('Failed records' in self.browser.contents)
+        self.assertFalse('Successfully created records' in self.browser.contents)
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl("Save").click()
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertFalse('Failed records' in self.browser.contents)
+        self.assertTrue('Successfully created records' in self.browser.contents)
+
+    def test_hidden_batch_copying_all(self):
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.prepare_applicant()
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        #self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        IWorkflowState(self.applicant).setState('admitted')
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        self.browser.open(self.root_path + '/createallstudents')
+        self.assertTrue('No course admitted provided' in self.browser.contents)
+        self.applicant.course_admitted = self.certificate
+        self.browser.open(self.root_path + '/createallstudents')
+        self.assertTrue('Successfully created records:' in self.browser.contents)
+
+    def test_copier_wo_passport(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        # Let's try to create the student
+        IWorkflowState(self.applicant).setState('admitted')
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue('created' in msg)
+
+    def test_copier_with_defective_passport_image(self):
+        IWorkflowState(self.applicant).setState('admitted')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        # Upload a passport picture
+        ctrl = self.browser.getControl(name='form.passport')
+        # This file has really been uploaded by KwaraPoly student
+        # Until 4/1/15 these files resulted a traceback when opening
+        # the createallstudents page. The traceback is now catched.
+        file_obj = open(
+            os.path.join(os.path.dirname(__file__), 'fake_image.jpg'),'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click() # submit form
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue(msg == 'IOError: Application Slip could not be created.')
+        # The same in the UI
+        self.browser.open(self.manage_container_path)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=self.applicant.application_number).selected = True
+        self.browser.getControl("Create students from selected", index=0).click()
+        self.assertTrue('No student could be created.' in self.browser.contents)
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue(
+            'IOError: Application Slip could not be created.'
+            in self.browser.contents)
+
+    def disabled_test_delay(self):
+        # Create portalmanager manager
+        self.app['users'].addUser('mrportalmanager', 'mrportalmanagersecret')
+        self.app['users']['mrportalmanager'].email = 'mrportalmanager@foo.ng'
+        self.app['users']['mrportalmanager'].title = 'Carlos Portales'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal(
+            'waeup.PortalManager', 'mrportalmanager')
+        # Login as portal manager
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrportalmanager'
+        self.browser.getControl(name="form.password").value = 'mrportalmanagersecret'
+        self.browser.getControl("Login").click()
+        self.prepare_applicant()
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        IWorkflowState(self.applicant).setState('admitted')
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        start = datetime.now()
+        self.browser.open(self.root_path + '/createallstudents')
+        self.assertTrue('1 students successfully created' in self.browser.contents)
+        self.assertTrue((datetime.now() - start).seconds >= 10)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_authentication.py	(revision 16045)
@@ -0,0 +1,185 @@
+## $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
+##
+import unittest
+from zope.authentication.interfaces import IAuthentication
+from zope.component import provideUtility, queryUtility, getGlobalSiteManager
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.password import SSHAPasswordManager
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth import PluggableAuthentication
+from zope.securitypolicy.role import Role
+from zope.securitypolicy.interfaces import IRole, Allow
+from waeup.kofa.authentication import get_principal_role_manager
+from waeup.kofa.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.kofa.applicants.authentication import (
+    ApplicantsAuthenticatorSetup, ApplicantAccount)
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+
+class ApplicantsAuthenticatorSetupTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = ApplicantsAuthenticatorSetup()
+        verifyClass(IAuthPluginUtility, ApplicantsAuthenticatorSetup)
+        verifyObject(IAuthPluginUtility, obj)
+        return
+
+    def test_register(self):
+        # Make sure registration works.
+        setup = ApplicantsAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        setup.register(pau)
+        self.assertTrue('applicants' in pau.authenticatorPlugins)
+        return
+
+    def test_unregister(self):
+        # Make sure deregistration works.
+        setup = ApplicantsAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        pau.authenticatorPlugins = ('applicants')
+        setup.unregister(pau)
+        self.assertTrue('applicants' not in pau.authenticatorPlugins)
+        return
+
+
+class FakeApplicant(object):
+    applicant_id = 'test_appl'
+    display_fullname = 'Tilman Gause'
+    password = None
+    email = None
+    phone = None
+    suspended = False
+
+
+class MinimalPAU(PluggableAuthentication):
+    def getPrincipal(self, id):
+        return 'faked principal'
+
+class ApplicantAccountTests(unittest.TestCase):
+
+    def setUp(self):
+        self.fake_stud = FakeApplicant()
+        self.account = ApplicantAccount(self.fake_stud)
+
+        # We provide a minimal PAU
+        pau = MinimalPAU()
+        provideUtility(pau, IAuthentication)
+
+        # We register a role
+        test_role = Role('waeup.test.Role', 'Testing Role')
+        provideUtility(test_role, IRole, name='waeup.test.Role')
+
+        # We have to setup a password manager utility manually as we
+        # have no functional test. In functional tests this would
+        # happen automatically, but it would take a lot more time to
+        # run the tests.
+        provideUtility(
+            SSHAPasswordManager(), IPasswordManager, 'SSHA')
+        return
+
+    def tearDown(self):
+        self.account.roles = [] # make sure roles are reset
+        gsm = getGlobalSiteManager()
+        to_clean = []
+        # Clear up utilities registered in setUp
+        to_clean.append(
+            (IPasswordManager, queryUtility(
+                    IPasswordManager, name='SSHA', default=None)))
+        to_clean.append(
+            (IAuthentication, queryUtility(IAuthentication, default=None)))
+        to_clean.append(
+            (IRole, queryUtility(IRole, name='test.Role', default=None)))
+        for iface, elem in to_clean:
+            if elem is not None:
+                gsm.unregisterUtility(elem, iface)
+        return
+
+    def test_iface(self):
+        verifyClass(IUserAccount, ApplicantAccount)
+        verifyObject(IUserAccount, self.account)
+        return
+
+    def test_set_password(self):
+        # make sure we can set a password.
+        self.account.setPassword('secret')
+        self.assertTrue(self.fake_stud.password is not None)
+        # we do not store plaintext passwords
+        self.assertTrue(self.fake_stud.password != 'secret')
+        # passwords are stored as bytestream
+        self.assertTrue(isinstance(self.fake_stud.password, basestring))
+        self.assertFalse(isinstance(self.fake_stud.password, unicode))
+        return
+
+    def test_check_password(self):
+        # make sure we can check a password.
+        self.account.setPassword('secret')
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword('nonsense')
+        result3 = self.account.checkPassword('secret')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        self.assertEqual(result3, True)
+        return
+
+    def test_check_unset_password(self):
+        # empty and unset passwords do not match anything
+        self.fake_stud.password = None
+        result1 = self.account.checkPassword('')
+        self.fake_stud.password = ''
+        result2 = self.account.checkPassword('')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_check_password_no_string(self):
+        # if passed in password is not a string, we gain no access
+        self.fake_stud.password = 'secret'
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword(object())
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_role_set(self):
+        # make sure we can set roles for principals denoted by account
+        prm = get_principal_role_manager()
+        self.assertEqual(prm.getPrincipalsAndRoles(), [])
+        self.account.roles = ['waeup.test.Role']
+        self.assertEqual(
+            prm.getPrincipalsAndRoles(),
+            [('waeup.test.Role', 'test_appl', Allow)])
+        return
+
+    def test_role_get(self):
+        # make sure we can get roles set for an account
+        self.assertEqual(self.account.roles, [])
+        self.account.roles = ['waeup.test.Role',] # set a role
+        self.assertEqual(self.account.roles, ['waeup.test.Role'])
+        return
+
+class FunctionalApplicantAuthTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalApplicantAuthTests, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalApplicantAuthTests, self).tearDown()
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,583 @@
+## $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
+##
+"""Unit tests for applicants-related data processors.
+"""
+import datetime
+import os
+import pytz
+import shutil
+import tempfile
+import unittest
+import grok
+from time import time
+from hurry.workflow.interfaces import IWorkflowState
+from zope.component.hooks import setSite, clearSite
+from zope.component import createObject
+from zope.interface.verify import verifyClass, verifyObject
+from zope.event import notify
+
+from waeup.kofa.app import University
+from waeup.kofa.applicants.batching import (
+    ApplicantsContainerProcessor, ApplicantProcessor,
+    ApplicantOnlinePaymentProcessor)
+from waeup.kofa.applicants.container import ApplicantsContainer
+from waeup.kofa.applicants.applicant import Applicant
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.interfaces import (
+    IBatchProcessor, IUserAccount, DuplicationError)
+from waeup.kofa.applicants.workflow import CREATED
+
+
+# Sample data we can use in tests...
+APPS_CONTAINER_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_container_data.csv'),
+    'rb').read()
+
+# The header fields of the above CSV snippet
+APPS_CONTAINER_HEADER_FIELDS = APPS_CONTAINER_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+# The same for applicants
+APPLICANT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_applicant_data.csv'),
+    'rb').read()
+FAULTY_APPLICANT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_faulty_applicant_data.csv'), 'rb').read()
+
+APPLICANT_HEADER_FIELDS = APPLICANT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+APPLICANT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_applicant_data_update.csv'), 'rb').read()
+
+APPLICANT_SAMPLE_DATA_UPDATE2 = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_applicant_data_update2.csv'), 'rb').read()
+
+APPLICANT_HEADER_FIELDS_UPDATE = APPLICANT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+APPLICANT_HEADER_FIELDS_UPDATE2 = APPLICANT_SAMPLE_DATA_UPDATE2.split(
+    '\n')[0].split(',')
+
+PAYMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_payment_data.csv'),
+    'rb').read()
+
+PAYMENT_HEADER_FIELDS = PAYMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class ApplicantsContainerProcessorTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantsContainerProcessorTest, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.container = ApplicantsContainer()
+        self.container.code = u'dp2011'
+        self.app['applicants']['dp2011'] = self.container
+
+        self.processor = ApplicantsContainerProcessor()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sampledata.csv')
+        open(self.csv_file, 'wb').write(APPS_CONTAINER_SAMPLE_DATA)
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        super(ApplicantsContainerProcessorTest, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantsContainerProcessor) is True
+
+    def test_parentsExist(self):
+        assert self.processor.parentsExist(None, dict()) is False
+        assert self.processor.parentsExist(None, self.app) is True
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(code='REG_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(code='dp2011'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['applicants']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(code='REG_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(code='dp2011'), self.app) is self.container
+
+    def test_addEntry(self):
+        self.processor.addEntry(
+            'New application', dict(code='dp2012'), self.app)
+        assert self.app['applicants']['dp2012'] == 'New application'
+
+    def test_delEntry(self):
+        self.processor.delEntry(dict(code='dp2011'), self.app)
+        assert 'dp2011' not in self.app['applicants'].keys()
+
+    def test_import(self):
+        # Do a real import
+        # see local sample_container.csv file for input
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, APPS_CONTAINER_HEADER_FIELDS)
+        avail_containers = [x for x in self.app['applicants'].keys()]
+        container = self.app['applicants'].get('app2017', None)
+        container2 = self.app['applicants'].get('app2018', None)
+        self.assertTrue(container is not None)
+        self.assertTrue(container2 is not None)
+
+        # check attributes
+        self.assertEqual(container.code, u'app2017')
+        self.assertEqual(container.title, u'General Studies')
+        self.assertEqual(container.prefix, u'app')
+        self.assertEqual(container.year, 2017)
+        self.assertEqual(container.application_category, 'basic')
+        self.assertEqual(
+            container.description,
+            u'This text can been seen by anonymous users.\n'
+            u'>>de<<\nDieser Text kann von anonymen Benutzern '
+            u'gelesen werden.')
+        self.assertEqual(container.startdate,
+                         datetime.datetime(2012, 3, 1, 0, 0, tzinfo=pytz.utc))
+        self.assertEqual(container.enddate,
+                         datetime.datetime(2012, 4, 25, 0, 0, tzinfo=pytz.utc))
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class ApplicantImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantImportExportSetup, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Add an applicants container
+        self.container = ApplicantsContainer()
+        self.container.code = u'dp2011'
+        self.container.application_category = u'basic'
+        self.app['applicants']['dp2011'] = self.container
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.start_level = 100
+        self.certificate.end_level = 500
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = 'CERT2'
+        self.certificate2.application_category = 'xyz'
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add applicant with subobjects
+        applicant = Applicant()
+        applicant.firstname = u'Anna'
+        applicant.lastname = u'Tester'
+        self.app['applicants']['dp2011'].addApplicant(applicant)
+        self.application_number = applicant.application_number
+        self.applicant = self.app['applicants']['dp2011'][
+            self.application_number]
+        self.workdir = tempfile.mkdtemp()
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        return
+
+    def tearDown(self):
+        super(ApplicantImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+class ApplicantProcessorTest(ApplicantImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantProcessorTest, self).setUp()
+        self.processor = ApplicantProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_applicant_data.csv')
+        self.csv_file_faulty = os.path.join(self.workdir,
+                                            'faulty_applicant_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_applicant_data_update.csv')
+        self.csv_file_update2 = os.path.join(
+            self.workdir, 'sample_applicant_data_update2.csv')
+        open(self.csv_file, 'wb').write(APPLICANT_SAMPLE_DATA)
+        open(self.csv_file_faulty, 'wb').write(FAULTY_APPLICANT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(APPLICANT_SAMPLE_DATA_UPDATE)
+        open(self.csv_file_update2, 'wb').write(APPLICANT_SAMPLE_DATA_UPDATE2)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantProcessor) is True
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(container_code='dp2011', application_number='999'),
+            self.app) is False
+
+    def test_getEntry(self):
+        applicant = self.processor.getEntry(
+            dict(container_code='dp2011',
+                 application_number=self.application_number), self.app)
+        self.assertEqual(applicant.applicant_id, self.applicant.applicant_id)
+
+    def test_addEntry(self):
+        new_applicant = Applicant()
+        self.processor.addEntry(
+            new_applicant, dict(container_code='dp2011'), self.app)
+        assert len(self.app['applicants']['dp2011'].keys()) == 2
+
+    def test_delEntry(self):
+        assert self.application_number in self.app[
+            'applicants']['dp2011'].keys()
+        self.processor.delEntry(
+            dict(container_code='dp2011',
+                application_number=self.application_number), self.app)
+        assert self.application_number not in self.app[
+            'applicants']['dp2011'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        keys = self.app['applicants']['dp2011'].keys()
+        assert len(keys) == 5
+        container = self.app['applicants']['dp2011']
+        assert  container.__implemented__.__name__ == (
+            'waeup.kofa.applicants.container.ApplicantsContainer')
+        applicant = container[keys[0]]
+        assert applicant.__implemented__.__name__ == (
+            'waeup.kofa.applicants.applicant.Applicant')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry,
+        # create applicant with given application_number
+        self.assertTrue(
+            'Applicant Processor - sample_applicant_data - imported: '
+            'applicant_id=dp2011_1234, password=mypwd1, '
+            'reg_number=1001, firstname=Aaren, middlename=Peter, lastname=Pieri, '
+            'sex=m, course1=CERT1, date_of_birth=1990-01-02, email=xx@yy.zz' in
+            logcontent)
+        # create applicant with random application_number which is
+        # not shown in the log file
+        self.assertTrue(
+            'Applicant Processor - sample_applicant_data - imported: '
+            'reg_number=1003, firstname=Aaren, '
+            'middlename=Alfons, lastname=Berson, sex=m, course1=CERT1, '
+            'date_of_birth=1990-01-04, email=xx@yy.zz' in
+            logcontent)
+        # Logging message from handle_applicant_transition_event
+        self.assertTrue(
+            'dp2011_1234 - Application initialized' in
+            logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_faulty(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_faulty, APPLICANT_HEADER_FIELDS)
+        # we cannot import data with faulty dates. A date is faulty
+        # when in format xx/yy/zzzz as we cannot say whether it is
+        # meant as dd/mm/yyyy or mm/dd/yyyy. We therefore require yyyy-mm-dd
+        for applicant in self.app['applicants']['dp2011'].values():
+            if applicant.date_of_birth == datetime.date(1990, 1, 2):
+                self.fail(
+                    'Wrong birthdate of imported applicant '
+                    '(1990-01-02, should be: 1990-02-01)')
+        self.assertEqual(num_warns,4)
+        fail_contents = open(fail_file, 'rb').read()
+        # CERT2 is in wrong category ...
+        self.assertTrue('course1: wrong application category' in fail_contents)
+        # ... and CERT3 does not exist.
+        self.assertTrue('course1: Invalid value' in fail_contents)
+        # Conversion checking already fails because Mister or Miss No's 
+        # container does not exist.
+        self.assertTrue('no@yy.zz,container: not found' in fail_contents)
+        self.assertTrue('nobody@yy.zz,container: not found' in fail_contents)
+        shutil.rmtree(os.path.dirname(fail_file))
+        return
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        # The middlename import value was None.
+        # Confirm that middlename has not been deleted.
+        container = self.app['applicants']['dp2011']
+        self.assertEqual(container['1234'].middlename, 'Peter')
+        # state of Pieri has not changed
+        self.assertEqual(container['1234'].state,'initialized')
+        # state of Finau has changed
+        self.assertEqual(container['2345'].state,'admitted')
+        # password of Pieri has been set
+        self.assertTrue(IUserAccount(container['1234']).checkPassword('mypwd1'))
+        # password of Finau is still unset
+        self.assertEqual(IUserAccount(container['2345']).password,None)
+        # password of Simon was encrypted already
+        self.assertTrue(
+            IUserAccount(container['4567']).checkPassword('mypwd1'))
+        # reg_number of Finau has changed
+        self.assertEqual(container['2345'].reg_number, '6666')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry,
+        # reg_number is locator
+        self.assertTrue(
+            'Applicant Processor - sample_applicant_data_update - updated: '
+            'reg_number=1001, firstname=Aaren' in
+            logcontent)
+        # applicant_id is locator
+        self.assertTrue(
+            'Applicant Processor - sample_applicant_data_update - updated: '
+            'state=admitted, reg_number=6666, '
+            'firstname=Alfons, applicant_id=dp2011_2345' in
+            logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Now we import another file which clears all middlename attributes
+        # and uses the new reg_number as locator. This test also checks
+        # if the catalog has been informed about the reg_no change and if
+        # applicants in state created are really blocked.
+        IWorkflowState(container['4567']).setState(CREATED)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update2, APPLICANT_HEADER_FIELDS_UPDATE2, 'update')
+        failcontent = open(fail_file).read()
+        self.assertTrue('Applicant is blocked' in failcontent)
+        self.assertEqual(num_warns,1)
+        # Middlename is cleared.
+        assert container['1234'].middlename is None
+        # Firstname of applicant in state created isn't changed.
+        self.assertEqual(container['4567'].firstname, 'Simon')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, 'remove')
+        self.assertEqual(num_warns,0)
+        logcontent = open(self.logfile).read()
+        # Logging message from handle_applicant_transition_event
+        self.assertTrue(
+            'dp2011_1234 - Application record removed' in
+            logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class PaymentProcessorTest(ApplicantImportExportSetup):
+
+    def setUp(self):
+        super(PaymentProcessorTest, self).setUp()
+
+        applicant = Applicant()
+        applicant.firstname = u'Anna2'
+        applicant.lastname = u'Tester'
+        applicant.applicant_id = u'dp2011_1234'
+        self.app['applicants']['dp2011'].addApplicant(applicant)
+        payment = createObject(u'waeup.ApplicantOnlinePayment')
+        payment.p_id = 'p120'
+        payment.p_session = 2012
+        payment.p_category = 'application'
+        payment.p_state = 'paid'
+        applicant['p120'] = payment
+        self.applicant2 = applicant
+        self.processor = ApplicantOnlinePaymentProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_payment_data.csv')
+        open(self.csv_file, 'wb').write(PAYMENT_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantOnlinePaymentProcessor) is True
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(applicant_id='ID_NONE', p_id='nonsense'), self.app) is None
+        assert self.processor.getEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app) is self.applicant2['p120']
+        assert self.processor.getEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120#'),
+            self.app) is self.applicant2['p120']
+
+    def test_delEntry(self):
+        assert self.processor.getEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app) is self.applicant2['p120']
+        self.assertEqual(len(self.applicant2.keys()),1)
+        self.processor.delEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app)
+        assert self.processor.getEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app) is None
+        self.assertEqual(len(self.applicant.keys()),0)
+
+    def test_addEntry(self):
+        self.assertEqual(len(self.applicant2.keys()),1)
+        self.processor.delEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app)
+        payment1 = createObject(u'waeup.ApplicantOnlinePayment')
+        payment1.p_category = 'application'
+        payment1.p_id = 'p234'
+        self.processor.addEntry(
+            payment1, dict(applicant_id=self.applicant2.applicant_id, p_id='p234'),
+            self.app)
+        self.assertEqual(len(self.applicant2.keys()),1)
+        self.assertEqual(self.applicant2['p234'].p_id, 'p234')
+        # Same payment must not exist.
+        payment1.p_state = 'paid'
+        payment2 = createObject(u'waeup.ApplicantOnlinePayment')
+        payment2.p_id = 'p456'
+        payment2.p_category = 'application'
+        self.assertRaises(
+            DuplicationError, self.processor.addEntry, payment2,
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p456'), self.app)
+        # But we can add a ticket with another p_category.
+        payment2.p_category = 'app_balance'
+        self.processor.addEntry(
+            payment2, dict(applicant_id=self.applicant2.applicant_id, p_id='p456'),
+            self.app)
+        self.assertEqual(len(self.applicant2.keys()),2)
+        self.assertEqual(self.applicant2['p456'].p_id, 'p456')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='<IGNORE>'), mode='create')
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='<IGNORE>'), mode='update')
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'missing'))
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='p1266236341955'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='nonsense'))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'invalid length'))
+        timestamp = ("%d" % int(time()*10000))[1:]
+        p_id = "p%s" % timestamp
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id=p_id))
+        self.assertEqual(len(errs),0)
+        dup_payment = createObject(u'waeup.ApplicantOnlinePayment')
+        dup_payment.p_id = 'p1266236341955'
+        self.applicant2[dup_payment.p_id] = dup_payment
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='p1266236341955'), mode='create')
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'p_id exists in dp2011_1234 '))
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,2)
+        fail_file = open(fail_file).read()
+        self.assertTrue('Payment has already been made' in fail_file)
+        self.processor.delEntry(
+            dict(applicant_id=self.applicant2.applicant_id, p_id='p120'),
+            self.app)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        # Both records can be imported now.
+        self.assertEqual(num_warns,0)
+        # One with imported and known p_id ...
+        payment = self.processor.getEntry(dict(applicant_id='dp2011_1234',
+            p_id='p1266236341955'), self.app)
+        self.assertEqual(payment.p_id, 'p1266236341955')
+        cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
+        # Ooooh, still the old problem, see
+        # http://mail.dzug.org/mailman/archives/zope/2006-August/001153.html.
+        # WAT is interpreted as GMT-1 and not GMT+1
+        self.assertEqual(cdate, '2010-11-25 21:16:33')
+        self.assertEqual(str(payment.creation_date.tzinfo),'UTC')
+        # ... the other one with generated p_id.
+        p_id_failed = self.applicant2.keys()[1]
+        payment_failed = self.processor.getEntry(dict(applicant_id='dp2011_1234',
+            p_id=p_id_failed), self.app)
+        self.assertEqual(payment_failed.p_state, 'failed')
+        self.assertEqual(payment_failed.amount_auth, 10500.1)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - ApplicantOnlinePayment Processor - dp2011_1234 - '
+            'previous update cancelled' in logcontent)
+        self.assertTrue(
+            'INFO - system - ApplicantOnlinePayment Processor - '
+            'sample_payment_data - dp2011_1234 - updated: p_id=p1266236341955, '
+            'creation_date=2010-11-25 21:16:33.757000+00:00, '
+            'r_amount_approved=19500.1, p_category=application, '
+            'amount_auth=19500.1, p_session=2015, p_state=paid' in logcontent)
+        self.assertTrue(
+            'INFO - system - ApplicantOnlinePayment Processor - '
+            'sample_payment_data - dp2011_1234 - updated: p_id=%s, '
+            'creation_date=2011-11-25 21:16:33.757000+00:00, '
+            'r_amount_approved=19500.1, p_category=application, '
+            'amount_auth=10500.1, p_session=2016, p_state=failed'
+            % p_id_failed in logcontent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_browser.py	(revision 16045)
@@ -0,0 +1,1847 @@
+## $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 applicant-related UI components.
+"""
+import os
+import pytz
+import shutil
+import tempfile
+import grok
+import logging
+from datetime import datetime
+from StringIO import StringIO
+from datetime import datetime, date, timedelta
+from mechanize import LinkNotFoundError
+from zc.async.testing import wait_for_result
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component import createObject, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.app import University
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.configuration import SessionConfiguration
+from waeup.kofa.refereeentries import RefereeEntry
+from waeup.kofa.applicants.container import ApplicantsContainer
+from waeup.kofa.applicants.applicant import Applicant
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser, IUserAccount, IJobManager)
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.mandates.mandate import RefereeReportMandate
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.tests.test_authentication import SECRET
+
+PH_LEN = 15911  # Length of placeholder file
+
+session_1 = datetime.now().year - 2
+container_name_1 = u'app%s' % session_1
+session_2 = datetime.now().year - 1
+container_name_2 = u'app%s' % session_2
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+
+class ApplicantsFullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for applicants handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a university, creates PINs,
+    # etc.  so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantsFullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.login_path = 'http://localhost/app/login'
+        self.root_path = 'http://localhost/app/applicants'
+        self.search_path = 'http://localhost/app/applicants/search'
+        self.manage_root_path = self.root_path + '/@@manage'
+        self.add_container_path = self.root_path + '/@@add'
+        self.container_path = 'http://localhost/app/applicants/%s' % container_name_1
+        self.manage_container_path = self.container_path + '/@@manage'
+
+        # Add an applicants container
+        applicantscontainer = ApplicantsContainer()
+        applicantscontainer.code = container_name_1
+        applicantscontainer.prefix = 'app'
+        applicantscontainer.year = session_1
+        applicantscontainer.title = u'This is the %s container' % container_name_1
+        applicantscontainer.application_category = 'basic'
+        applicantscontainer.mode = 'create'
+        applicantscontainer.strict_deadline = True
+        delta = timedelta(days=10)
+        applicantscontainer.startdate = datetime.now(pytz.utc) - delta
+        applicantscontainer.enddate = datetime.now(pytz.utc) + delta
+        self.app['applicants'][container_name_1] = applicantscontainer
+        self.applicantscontainer = self.app['applicants'][container_name_1]
+
+        # Populate university
+        certificate = createObject('waeup.Certificate')
+        certificate.code = 'CERT1'
+        certificate.application_category = 'basic'
+        certificate.start_level = 100
+        certificate.end_level = 500
+        certificate.study_mode = u'ug_ft'
+        self.certificate = certificate
+        self.app['faculties']['fac1'] = Faculty()
+        # The code has explicitely to be set, otherwise we don't
+        # find created students in their department
+        self.app['faculties']['fac1']['dep1'] = Department(code=u'dep1')
+        self.department = self.app['faculties']['fac1']['dep1']
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            certificate)
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+        # Create 5 access codes with prefix'FOO' and cost 9.99 each
+        pin_container = self.app['accesscodes']
+        pin_container.createBatch(
+            datetime.now(), 'some_userid', 'APP', 9.99, 5)
+        pins = pin_container[pin_container.keys()[0]].values()
+        self.pins = [x.representation for x in pins]
+        self.existing_pin = self.pins[0]
+        parts = self.existing_pin.split('-')[1:]
+        self.existing_series, self.existing_number = parts
+
+        # Add an applicant
+        self.applicant = createObject('waeup.Applicant')
+        # reg_number is the only field which has to be preset here
+        # because managers are allowed to edit this required field
+        self.applicant.firstname = u'Joan'
+        self.applicant.reg_number = u'1234'
+        self.applicant.course1 = certificate
+        app['applicants'][container_name_1].addApplicant(self.applicant)
+        IUserAccount(
+            self.app['applicants'][container_name_1][
+            self.applicant.application_number]).setPassword('apwd')
+        self.manage_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            container_name_1, self.applicant.application_number, 'manage')
+        self.edit_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            container_name_1, self.applicant.application_number, 'edit')
+        self.view_path = 'http://localhost/app/applicants/%s/%s' % (
+            container_name_1, self.applicant.application_number)
+
+    def login(self):
+        # Perform an applicant login. This creates an application record.
+        #
+        # This helper also sets `self.applicant`, which is the
+        # applicant object created.
+        self.browser.open(self.login_path)
+        self.browser.getControl(
+            name="form.login").value = self.applicant.applicant_id
+        self.browser.getControl(name="form.password").value = 'apwd'
+        self.browser.getControl("Login").click()
+
+    def fill_correct_values(self):
+        # Fill the edit form with suitable values
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.middlename").value = 'Anthony'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.course1").value = ['CERT1']
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl(name="form.sex").value = ['m']
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+
+    def tearDown(self):
+        super(ApplicantsFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+class ApplicantsRootUITests(ApplicantsFullSetup):
+    # Tests for ApplicantsRoot class
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can access applicants root
+        self.browser.open(self.root_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertFalse(
+            'Manage ' in self.browser.contents)
+        return
+
+    def test_anonymous_no_actions(self):
+        # Make sure anonymous users cannot access actions
+        self.browser.open(self.root_path)
+        self.assertRaises(
+            LookupError, self.browser.getControl, "Add local role")
+        # Manage screen neither linked nor accessible for anonymous
+        self.assertRaises(
+            LinkNotFoundError,
+            self.browser.getLink, 'Manage applicants section')
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_root_path)
+        # Add container screen not accessible for anonymous
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.add_container_path)
+        return
+
+    def test_manage_access(self):
+        # Managers can access the manage pages of applicants root
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.root_path)
+        self.assertTrue('Manage applicants section' in self.browser.contents)
+        # There is a manage link
+        link = self.browser.getLink('Manage applicants section')
+        link.click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_root_path)
+        return
+
+    def test_hide_container(self):
+        self.browser.open(self.root_path)
+        self.assertTrue(
+            '<a href="http://localhost/app/applicants/%s">'
+            'This is the %s container</a>' % (container_name_1, container_name_1)
+            in self.browser.contents)
+        self.app['applicants'][container_name_1].hidden = True
+        self.browser.open(self.root_path)
+        # Anonymous users can't see hidden containers
+        self.assertFalse(
+            '<a href="http://localhost/app/applicants/%s">'
+            'This is the %s container</a>' % (container_name_1, container_name_1)
+            in self.browser.contents)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.root_path)
+        self.assertTrue(
+            '<a href="http://localhost/app/applicants/%s">'
+            'This is the %s container</a>' % (container_name_1, container_name_1)
+            in self.browser.contents)
+        return
+
+    def test_search(self):
+        # Managers can access the manage pages of applicants root
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        self.browser.open(self.root_path)
+        self.assertTrue('Manage applicants section' in self.browser.contents)
+        # There is a search link
+        link = self.browser.getLink('Find applicants')
+        link.click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # We can find an applicant ...
+        # ... via his name
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'John'
+        self.browser.getControl("Find applicant").click()
+        self.assertTrue('John Anthony Tester' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester'
+        self.browser.getControl("Find applicant").click()
+        self.assertTrue('John Anthony Tester' in self.browser.contents)
+        self.browser.open(self.search_path)
+        # ... and via his reg_number ...
+        self.browser.getControl(name="searchtype").value = ['reg_number']
+        self.browser.getControl(name="searchterm").value = '2345'
+        self.browser.getControl("Find applicant").click()
+        self.assertFalse('John Anthony Tester' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['reg_number']
+        self.browser.getControl(name="searchterm").value = '1234'
+        self.browser.getControl("Find applicant").click()
+        self.assertTrue('John Anthony Tester' in self.browser.contents)
+        # ... and not via his application_number ...
+        self.browser.getControl(name="searchtype").value = ['applicant_id']
+        self.browser.getControl(
+            name="searchterm").value = self.applicant.application_number
+        self.browser.getControl("Find applicant").click()
+        self.assertFalse('John Anthony Tester' in self.browser.contents)
+        # ... but ia his applicant_id ...
+        self.browser.getControl(name="searchtype").value = ['applicant_id']
+        self.browser.getControl(
+            name="searchterm").value = self.applicant.applicant_id
+        self.browser.getControl("Find applicant").click()
+        self.assertTrue('John Anthony Tester' in self.browser.contents)
+        # ... and via his email
+        self.browser.getControl(name="searchtype").value = ['email']
+        self.browser.getControl(name="searchterm").value = 'xx@yy.zz'
+        self.browser.getControl("Find applicant").click()
+        self.assertTrue('John Anthony Tester' in self.browser.contents)
+        return
+
+    def test_manage_actions_access(self):
+        # Managers can access the action on manage screen
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_root_path)
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('No user selected' in self.browser.contents)
+        return
+
+    def test_local_roles_add_delete(self):
+        # Managers can assign and delete local roles of applicants root
+        myusers = self.app['users']
+        myusers.addUser('bob', 'bobssecret')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/faculties/fac1/dep1/manage')
+        self.browser.getControl(name="user").value = ['bob']
+        self.browser.getControl(name="local_role").value = [
+            'waeup.local.ApplicationsManager']
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('<td>bob</td>' in self.browser.contents)
+        # Remove the role assigned
+        ctrl = self.browser.getControl(name='role_id')
+        ctrl.getControl(
+            value='bob|waeup.local.ApplicationsManager').selected = True
+        self.browser.getControl("Remove selected local roles").click()
+        self.assertTrue(
+            'Local role successfully removed: bob|waeup.local.ApplicationsManager'
+            in self.browser.contents)
+        self.assertFalse('<td>bob</td>' in self.browser.contents)
+        return
+
+    def test_add_delete_container(self):
+        # Managers can add and delete applicants containers
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_root_path)
+        self.browser.getControl("Add applicants container").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_container_path)
+        self.browser.getControl(name="form.prefix").value = ['app']
+        self.browser.getControl("Add applicants container").click()
+        self.assertTrue(
+            'There were errors' in self.browser.contents)
+        self.browser.getControl(name="form.prefix").value = ['app']
+        self.browser.getControl(name="form.year").value = [str(session_2)]
+        self.browser.getControl(name="form.mode").value = ['create']
+        self.browser.getControl(
+            name="form.application_category").value = ['basic']
+        self.browser.getControl("Add applicants container").click()
+        self.assertTrue('Added:' in self.browser.contents)
+        self.browser.getLink(container_name_1).click()
+        self.assertTrue('Manage applicants container'
+            in self.browser.contents)
+        self.browser.open(self.add_container_path)
+        self.browser.getControl("Cancel").click()
+        self.assertEqual(self.browser.url, self.manage_root_path)
+        self.browser.open(self.add_container_path)
+        self.browser.getControl(name="form.prefix").value = ['app']
+        self.browser.getControl(name="form.year").value = [str(session_2)]
+        self.browser.getControl(name="form.mode").value = ['create']
+        self.browser.getControl(
+            name="form.application_category").value = ['basic']
+        self.browser.getControl("Add applicants container").click()
+        self.assertTrue('exists already in the database'
+                        in self.browser.contents)
+        # Managers can add containers with prefix+number code instead of
+        # prefix+year code.
+        self.browser.open(self.add_container_path)
+        self.browser.getControl(name="form.prefix").value = ['app']
+        self.browser.getControl(name="form.year").value = [str(session_2)]
+        self.browser.getControl(name="form.container_number").value = ['8']
+        self.browser.getControl(name="form.mode").value = ['create']
+        self.browser.getControl(
+            name="form.application_category").value = ['basic']
+        self.browser.getControl("Add applicants container").click()
+        self.assertTrue('Added: "app8"' in self.browser.contents)
+        self.browser.getLink(container_name_1).click()
+        self.assertTrue('Manage applicants container'
+            in self.browser.contents)
+        self.browser.open(self.manage_root_path)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=container_name_2).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed:' in self.browser.contents)
+        self.browser.open(self.add_container_path)
+        self.browser.getControl(name="form.prefix").value = ['app']
+        self.browser.getControl(name="form.year").value = [str(session_2)]
+        self.browser.getControl(name="form.mode").value = ['create']
+        #self.browser.getControl(name="form.ac_prefix").value = ['APP']
+        self.browser.getControl(
+            name="form.application_category").value = ['basic']
+        self.browser.getControl("Add applicants container").click()
+        del self.app['applicants'][container_name_2]
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=container_name_2).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertMatches('...Could not delete...', self.browser.contents)
+        return
+
+class ApplicantsContainerUITests(ApplicantsFullSetup):
+    # Tests for ApplicantsContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can access applicants containers
+        self.browser.open(self.container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertFalse(
+            'Manage ' in self.browser.contents)
+        return
+
+    def test_manage_access(self):
+        # Managers can access the manage pages of applicants
+        # containers and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_container_path)
+        self.browser.getControl(name="form.application_fee").value = '200'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ApplicantsContainerManageFormPage - '
+            '%s - saved: application_fee\n' % container_name_1 in logcontent)
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('No applicant selected' in self.browser.contents)
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('No user selected' in self.browser.contents)
+        self.browser.getControl("Cancel", index=0).click()
+        self.assertEqual(self.browser.url, self.container_path)
+        return
+
+    def test_statistics(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.container_path)
+        self.browser.getLink("Container statistics").click()
+        self.assertTrue('<td>initialized</td>' in self.browser.contents)
+        self.assertTrue('<td>1</td>' in self.browser.contents)
+        self.assertEqual(self.applicantscontainer.statistics[0],
+            {'not admitted': 0, 'started': 0, 'created': 0,
+            'admitted': 0, 'submitted': 0, 'initialized': 1,
+            'paid': 0, 'processed': 0})
+        #self.assertEqual(self.applicantscontainer.statistics[1],
+        #    {u'fac1': 0})
+        IWorkflowState(self.applicant).setState('submitted')
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        self.assertEqual(self.applicantscontainer.statistics[0],
+            {'not admitted': 0, 'started': 0, 'created': 0,
+            'admitted': 0, 'submitted': 1, 'initialized': 0, 'paid': 0,
+            'processed': 0})
+        #self.assertEqual(self.applicantscontainer.statistics[1],
+        #    {u'fac1': 1})
+        return
+
+    def test_add_delete_applicants(self):
+        # Check the global role map first
+        role_manager = IPrincipalRoleManager(grok.getSite())
+        principals = role_manager.getPrincipalsForRole('waeup.Applicant')
+        self.assertEqual(len(principals), 1)
+        self.assertEqual(principals[0][0], self.applicant.applicant_id)
+        # Managers can add and delete applicants
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.add_applicant_path = self.container_path + '/addapplicant'
+        self.container_manage_path = self.container_path + '/@@manage'
+        self.browser.open(self.container_manage_path)
+        self.browser.getLink("Add applicant").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_applicant_path)
+        self.browser.getControl(name="form.firstname").value = 'Alois'
+        self.browser.getControl(name="form.middlename").value = 'Kofi'
+        self.browser.getControl(name="form.lastname").value = 'Bettermann'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Create application record").click()
+        self.assertTrue('Application initialized' in self.browser.contents)
+        # The global role map has been extended
+        role_manager = IPrincipalRoleManager(grok.getSite())
+        principals = role_manager.getPrincipalsForRole('waeup.Applicant')
+        self.assertEqual(len(principals), 2)
+        self.browser.open(self.container_manage_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed:' in self.browser.contents)
+        # The global role map has been reduced
+        role_manager = IPrincipalRoleManager(grok.getSite())
+        principals = role_manager.getPrincipalsForRole('waeup.Applicant')
+        self.assertEqual(len(principals), 1)
+        self.browser.open(self.add_applicant_path)
+        self.browser.getControl(name="form.firstname").value = 'Albert'
+        self.browser.getControl(name="form.lastname").value = 'Einstein'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Create application record").click()
+        self.assertTrue('Application initialized' in self.browser.contents)
+        return
+
+    def test_prefill_purge_container(self):
+        # Managers can pre-fill containers in create mode
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.prefill_path = self.container_path + '/prefill'
+        self.container_manage_path = self.container_path + '/@@manage'
+        self.browser.open(self.container_manage_path)
+        self.browser.getLink("Pre-fill").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.prefill_path)
+        self.browser.getControl(name="number").value = ['10']
+        self.browser.getControl("Pre-fill").click()
+        self.assertTrue('10 application records created.' in self.browser.contents)
+        self.browser.open(self.container_manage_path)
+        self.assertTrue('This container contains 10 unused pre-filled records.'
+            in self.browser.contents)
+        self.assertEqual(self.applicantscontainer.counts[0], 11)
+        self.assertEqual(self.applicantscontainer.counts[1], 1)
+        # In update mode we can't pre-fill the container
+        self.applicantscontainer.mode = 'update'
+        self.browser.open(self.container_manage_path)
+        self.browser.getLink("Pre-fill").click()
+        self.assertTrue('Container must be in create mode to be pre-filled.'
+            in self.browser.contents)
+        self.browser.open(self.manage_root_path)
+        # Number of total records is 11
+        self.assertTrue('<td>11</td>' in self.browser.contents)
+        # The statistics have not changed
+        self.browser.open(self.container_path)
+        self.browser.getLink("Container statistics").click()
+        self.assertTrue('<td>1</td>' in self.browser.contents)
+        self.assertEqual(self.applicantscontainer.statistics[0],
+            {'not admitted': 0, 'started': 0, 'created': 0,
+            'admitted': 0, 'submitted': 0, 'initialized': 1,
+            'paid': 0, 'processed': 0})
+        # Container can be purged
+        IWorkflowState(self.applicant).setState('submitted')
+        self.browser.open(self.container_manage_path)
+        self.browser.getLink("Purge").click()
+        self.browser.getControl("Remove").click()
+        self.assertTrue('10 application records purged' in self.browser.contents)
+        self.assertEqual(self.applicantscontainer.counts[0], 1)
+        self.assertEqual(self.applicantscontainer.counts[1], 1)
+        IWorkflowState(self.applicant).setState('initialized')
+        self.browser.open(self.container_manage_path)
+        self.browser.getLink("Purge").click()
+        self.browser.getControl("Remove").click()
+        self.assertTrue('1 application records purged' in self.browser.contents)
+        self.assertEqual(self.applicantscontainer.counts[0], 0)
+        self.assertEqual(self.applicantscontainer.counts[1], 0)
+        return
+
+    def init_officer(self):
+        # Create application officer
+        self.app['users'].addUser('mrappl', SECRET)
+        self.app['users']['mrappl'].email = 'mrappl@foo.ng'
+        self.app['users']['mrappl'].title = 'Carlo Pitter'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.ApplicationsManager', 'mrappl')
+        # Login as officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrappl'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+
+    def test_student_creation_permission(self):
+        self.init_officer()
+        self.browser.open(self.container_path + '/manage')
+        self.browser.getControl("Create students").click()
+        self.assertTrue('You don\'t have permission to create student records'
+            in self.browser.contents)
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.StudentsCreator', 'mrappl')
+        self.browser.getControl("Create students").click()
+        self.assertTrue('No applicant selected' in self.browser.contents)
+
+
+class ApplicantUITests(ApplicantsFullSetup):
+    # Tests for uploading/browsing the passport image of appplicants
+
+    layer = FunctionalLayer
+
+    def test_manage_and_view_applicant(self):
+        # Managers can manage applicants
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.slip_path = self.view_path + '/application_slip.pdf'
+        self.browser.open(self.manage_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.fill_correct_values()
+        # Fire transition
+        self.browser.getControl(name="transition").value = ['start']
+        self.browser.getControl("Save").click()
+        # Be sure that the empty phone field does not show wrong error message
+        self.assertFalse('Required input is missing' in self.browser.contents)
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.assertMatches('...Application started by Manager...',
+                           self.browser.contents)
+        self.browser.open(self.view_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # Change course_admitted
+        self.browser.open(self.manage_path)
+        self.browser.getControl(name="form.course_admitted").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        # Change password
+        self.browser.getControl(name="password").value = 'secret'
+        self.browser.getControl(name="control_password").value = 'secre'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Passwords do not match...',
+                           self.browser.contents)
+        self.browser.getControl(name="password").value = 'secret'
+        self.browser.getControl(name="control_password").value = 'secret'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        # Pdf slip can't be opened and download button is not available
+        self.assertFalse('Download application slip' in self.browser.contents)
+        self.browser.open(self.slip_path)
+        self.assertTrue(
+            'Please pay and submit before trying to download the application slip.'
+            in self.browser.contents)
+        # If applicant is in correct state the pdf slip can be opened.
+        IWorkflowState(self.applicant).setState('submitted')
+        self.browser.open(self.manage_path)
+        self.browser.getLink("Download application slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        # Managers can view applicants even if certificate has been removed
+        del self.app['faculties']['fac1']['dep1'].certificates['CERT1']
+        self.browser.open(self.view_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.open(self.slip_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_passport_edit_view(self):
+        # We get a default image after login
+        self.browser.open(self.login_path)
+        self.login()
+        self.browser.open(self.browser.url + '/passport.jpg')
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertEqual(self.browser.headers['content-type'], 'image/jpeg')
+        self.assertTrue('JFIF' in self.browser.contents)
+        self.assertEqual(
+            self.browser.headers['content-length'], str(PH_LEN))
+
+    def test_applicant_login(self):
+        self.applicant.suspended = True
+        self.login()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        self.applicant.suspended = False
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+
+    def test_maintenance_mode(self):
+        config = grok.getSite()['configuration']
+        self.login()
+        # Applicant  logged in.
+        self.assertTrue('You logged in' in self.browser.contents)
+        self.assertTrue("Joan None" in self.browser.contents)
+        # If maintenance mode is enabled, applicant is immediately logged out.
+        config.maintmode_enabled_by = u'any_user'
+        self.assertRaises(
+            Unauthorized, self.browser.open, 'http://localhost/app/faculties')
+        self.browser.open('http://localhost/app/login')
+        self.assertTrue('The portal is in maintenance mode' in self.browser.contents)
+        # Applicant really can't login if maintenance mode is enabled.
+        self.login()
+        # A second warning is raised.
+        self.assertTrue(
+            'The portal is in maintenance mode. You can\'t login!'
+            in self.browser.contents)
+        return
+
+    def test_applicant_access(self):
+        # Applicants can edit their record
+        self.browser.open(self.login_path)
+        self.login()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+        self.browser.open(self.edit_path)
+        self.assertTrue(self.browser.url != self.login_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.fill_correct_values()
+        self.assertTrue(IUserAccount(self.applicant).checkPassword('apwd'))
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        # Applicants don't see manage and search links ...
+        self.browser.open(self.root_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertFalse('Search' in self.browser.contents)
+        self.assertFalse('Manage applicants section' in self.browser.contents)
+        # ... and can't access the manage page
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_path)
+        return
+
+    def test_message_for_created(self):
+        IWorkflowState(self.applicant).setState('created')
+        self.applicant.student_id = u'my id'
+        self.browser.open(self.login_path)
+        self.login()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+        self.assertTrue(
+            '<strong>Congratulations!</strong> You have been offered provisional'
+            ' admission into the %s/%s Academic Session of'
+            ' Sample University. Your student record has been created for you.'
+            % (session_1, session_1 + 1) in self.browser.contents)
+        self.assertTrue(
+            'Then enter your new student credentials: user name= my id,'
+            ' password = %s.' % self.applicant.application_number
+            in self.browser.contents)
+        return
+
+    def image_url(self, filename):
+        return self.edit_path.replace('edit', filename)
+
+    def test_after_login_default_browsable(self):
+        # After login we see the placeholder image in the edit view
+        self.login()
+        self.assertEqual(self.browser.url, self.view_path)
+        self.browser.open(self.edit_path)
+        # There is a correct <img> link included
+        self.assertTrue(
+              '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('passport.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+
+    def test_after_submit_default_browsable(self):
+        # After submitting an applicant form the default image is
+        # still visible
+        self.login()
+        self.browser.open(self.edit_path)
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('passport.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+
+    def test_uploaded_image_respects_file_size_restriction(self):
+        # When we upload an image that is too big ( > 10 KB) we will
+        # get an error message
+        self.login()
+        self.browser.open(self.edit_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        photo_content = 'A' * 1024 * 51  # A string of 11 KB size
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # We get a warning message
+        self.assertTrue(
+            'Uploaded image is too big' in self.browser.contents)
+        # Browsing the image gives us the default image, not the
+        # uploaded one.
+        self.browser.open(self.image_url('passport.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+        # There is really no file stored for the applicant
+        img = getUtility(IExtFileStore).getFile(
+            IFileStoreNameChooser(self.applicant).chooseName())
+        self.assertTrue(img is None)
+
+    def test_uploaded_image_browsable_w_errors(self):
+        # We can upload a different image and browse it after submit,
+        # even if there are still errors in the form
+        self.login()
+        self.browser.open(self.edit_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        photo_content = 'I pretend to be a graphics file'
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('passport.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(self.browser.contents, photo_content)
+
+    def test_uploaded_image_stored_in_imagestorage_w_errors(self):
+        # After uploading a new passport pic the file is correctly
+        # stored in an imagestorage
+        self.login()
+        self.browser.open(self.edit_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        pseudo_image.seek(0) # reset our file data source
+        self.assertEqual(
+            storage.getFile(file_id).read(), pseudo_image.read())
+        return
+
+    def test_uploaded_image_browsable_wo_errors(self):
+        # We can upload a different image and browse it after submit,
+        # if there are no errors in form
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        # Create a pseudo image file and select it to be uploaded in form
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="passport.jpg" height="180px" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('passport.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), 31)
+
+    def test_uploaded_image_stored_in_imagestorage_wo_errors(self):
+        # After uploading a new passport pic the file is correctly
+        # stored in an imagestorage if form contains no errors
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        # Create a pseudo image file and select it to be uploaded in form
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        # The stored image can be fetched
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 31)
+        # When an applicant is removed, also the image is gone.
+        del self.app['applicants'][container_name_1][self.applicant.application_number]
+        fd = storage.getFile(file_id)
+        self.assertTrue(fd is None)
+
+    def test_uploaded_images_equal(self):
+        # Make sure uploaded images do really differ if we eject a
+        # change notfication (and do not if we don't)
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        self.browser.getControl("Save").click() # submit form
+        # Now go on as an officer
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+
+        # Create a pseudo image file and select it to be uploaded in form
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        setSite(self.app)
+        passport0 = getUtility(IExtFileStore).getFile(file_id)
+        self.browser.getControl("Save").click() # submit form with changed pic
+        passport1 = getUtility(IExtFileStore).getFile(file_id).read()
+        self.browser.getControl("Save").click() # submit form w/o changes
+        passport2 = getUtility(IExtFileStore).getFile(file_id).read()
+        self.assertTrue(passport0 is None)
+        self.assertTrue(passport0 != passport1)
+        self.assertTrue(passport1 == passport2)
+        return
+
+    def test_upload_image_by_manager_with_logging(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        photo_content = 'A' * 1024 * 5  # A string of 5 KB size
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        # Even though the form could not be saved ...
+        self.assertTrue(
+            'Required input is missing' in self.browser.contents)
+        # ... the file has been successfully uploaded
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ApplicantManageFormPage - '
+            '%s - saved: passport'
+            % (self.applicant.applicant_id)
+            in logcontent)
+
+    def test_application_slip_with_non_jpg_image(self):
+        IWorkflowState(self.applicant).setState('submitted')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        # Create a pseudo image file and select it to be uploaded in form
+        photo_content = 'A' * 1024 * 5  # A string of 5 KB size
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click() # submit form
+        self.browser.open(self.manage_path)
+        self.browser.getLink("Download application slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertMatches(
+            '...Your image file is corrupted. Please replace...',
+            self.browser.contents)
+
+    def test_pay_portal_application_fee(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values()
+        # We have to save the form otherwise the filled fields will be cleared
+        # after adding an online payment, because adding an online payment
+        # requires a filled form but does not save it
+        self.browser.getControl("Save").click()
+        # Payment section does not appear if application fee isn't set
+        self.assertFalse('Payment Tickets' in self.browser.contents)
+        self.assertFalse('Add online payment' in self.browser.contents)
+        self.applicantscontainer.application_fee = 200.0
+        self.browser.open(self.edit_path)
+        self.browser.getControl("Add online payment ticket").click()
+        # Session object missing
+        self.assertTrue(
+            'Session configuration object is not available'
+            in self.browser.contents)
+        configuration = SessionConfiguration()
+        configuration.academic_session = session_1
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.open(self.edit_path)
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        self.assertMatches('...Activation Code...',
+                           self.browser.contents)
+        self.assertTrue(
+            '<span>200.0</span>' in self.browser.contents)
+        # Payment ticket can be removed if they haven't received a
+        # valid callback
+        self.browser.open(self.edit_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertMatches('...Successfully removed...', self.browser.contents)
+        # We will try the callback request view
+        self.browser.getControl("Add online payment ticket").click()
+        self.browser.open(self.edit_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        payment_url = self.browser.url
+        payment_id = self.applicant.keys()[0]
+        payment = self.applicant[payment_id]
+        self.assertEqual(payment.p_item,'This is the %s container' % container_name_1)
+        self.assertEqual(payment.p_session, session_1)
+        self.assertEqual(payment.p_category,'application')
+        self.assertEqual(payment.amount_auth,200.0)
+        # Applicant is payer of the payment ticket.
+        self.assertEqual(
+            IPayer(payment).display_fullname, 'John Anthony Tester')
+        self.assertEqual(
+            IPayer(payment).id, self.applicant.applicant_id)
+        self.assertEqual(IPayer(payment).faculty, 'N/A')
+        self.assertEqual(IPayer(payment).department, 'N/A')
+        # The pdf payment slip can't yet be opened
+        #self.browser.open(payment_url + '/payment_receipt.pdf')
+        #self.assertMatches('...Ticket not yet paid...',
+        #                   self.browser.contents)
+        # Approve payment
+        # Applicants can't approve payments
+        self.assertRaises(
+            Unauthorized, self.browser.open, payment_url + '/approve')
+        # We approve the payment by bypassing the view
+        payment.approve()
+        # Applicant is is not yet in state 'paid' because it was only
+        # the payment which we set to paid
+        self.browser.open(self.view_path)
+        self.assertMatches('...started...',
+                           self.browser.contents)
+        self.assertTrue(self.applicant.state == 'started')
+        # Let's logout and approve the payment as manager
+        self.browser.getLink("Logout").click()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # First we reset the payment
+        payment.r_amount_approved = 0.0
+        payment.r_code = u''
+        payment.p_state = 'unpaid'
+        payment.r_desc = u''
+        payment.payment_date = None
+        self.browser.open(payment_url)
+        self.browser.getLink("Approve payment").click()
+        self.assertEqual(payment.p_state, 'paid')
+        self.assertEqual(payment.r_amount_approved, 200.0)
+        self.assertEqual(payment.r_code, 'AP')
+        self.assertTrue(self.applicant.state == 'paid')
+        # Approval is logged in students.log ...
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - applicants.browser.OnlinePaymentApprovePage - '
+            '%s - approved' % self.applicant.applicant_id
+            in logcontent)
+        # ... and in payments.log
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'payments.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            '"zope.mgr",%s,%s,application,200.0,AP,,,,,,\n'
+            % (self.applicant.applicant_id, payment.p_id)
+            in logcontent)
+        # Payment slips can't be downloaded ...
+        payment_id = self.applicant.keys()[0]
+        self.browser.open(self.view_path + '/' + payment_id)
+        self.browser.getLink("Download payment slip").click()
+        self.assertTrue(
+            'Please submit the application form before trying to download payment slips.'
+            in self.browser.contents)
+        # ... unless form is submitted.
+        self.browser.open(self.view_path + '/edit')
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='myphoto.jpg')
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        self.browser.open(self.view_path + '/' + payment_id)
+        self.browser.getLink("Download payment slip").click()
+        self.assertEqual(self.browser.headers['Content-Type'],
+                 'application/pdf')
+        return
+
+    def prepare_special_container(self):
+        # Add special application container
+        container_name = u'special%s' % session_1
+        applicantscontainer = ApplicantsContainer()
+        applicantscontainer.code = container_name
+        applicantscontainer.prefix = 'special'
+        applicantscontainer.year = session_1
+        applicantscontainer.title = u'This is a special app container'
+        applicantscontainer.application_category = 'no'
+        applicantscontainer.mode = 'create'
+        applicantscontainer.strict_deadline = True
+        delta = timedelta(days=10)
+        applicantscontainer.startdate = datetime.now(pytz.utc) - delta
+        applicantscontainer.enddate = datetime.now(pytz.utc) + delta
+        self.app['applicants'][container_name] = applicantscontainer
+        # Add an applicant
+        applicant = createObject('waeup.Applicant')
+        # reg_number is the only field which has to be preset here
+        # because managers are allowed to edit this required field
+        applicant.reg_number = u'12345'
+        self.special_applicant = applicant
+        self.app['applicants'][container_name].addApplicant(applicant)
+        IUserAccount(
+            self.app['applicants'][container_name][
+            applicant.application_number]).setPassword('apwd')
+        # Add session configuration object
+        self.configuration = SessionConfiguration()
+        self.configuration.academic_session = session_1
+        #self.configuration.transcript_fee = 200.0
+        self.configuration.clearance_fee = 300.0
+        self.app['configuration'].addSessionConfiguration(self.configuration)
+
+
+    def test_pay_special_fee(self):
+        self.prepare_special_container()
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(
+            name="form.login").value = self.special_applicant.applicant_id
+        self.browser.getControl(name="form.password").value = 'apwd'
+        self.browser.getControl("Login").click()
+        applicant_path = self.browser.url
+        self.browser.getLink("Edit application record").click()
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.middlename").value = 'Anthony'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.special_application").value = [
+            'transcript']
+        self.browser.getControl(name="form.date_of_birth").value = '09/09/1988'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.configuration.transcript_fee = 0.0
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Amount could not be determined...',
+                           self.browser.contents)
+        self.configuration.transcript_fee = 200.0
+        self.browser.getLink("Edit application record").click()
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        self.assertTrue(
+            '<span>Transcript Fee</span>' in self.browser.contents)
+        self.assertTrue(
+            'This is a special app container' in self.browser.contents)
+        self.assertTrue(
+            '<span>200.0</span>' in self.browser.contents)
+        self.assertEqual(len(self.special_applicant.keys()), 1)
+        # The applicant's workflow state is paid ...
+        self.special_applicant.payments[0].approveApplicantPayment()
+        self.assertEqual(self.special_applicant.state, 'paid')
+        self.browser.open(applicant_path + '/edit')
+        # ... but he can create further tickets.
+        self.browser.getControl(name="form.special_application").value = [
+            'clearance']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        self.browser.open(applicant_path)
+        self.assertTrue(
+            '<td>Acceptance Fee</td>' in self.browser.contents)
+        self.assertEqual(len(self.special_applicant.keys()), 2)
+        # Second payment can also be approved wthout error message
+        flashtype, msg, log = self.special_applicant.payments[1].approveApplicantPayment()
+        self.assertEqual(flashtype, 'success')
+        self.assertEqual(msg, 'Payment approved')
+        # Payment slips can't be downloaded ...
+        payment_id = self.special_applicant.keys()[0]
+        self.browser.open(applicant_path + '/' + payment_id)
+        self.browser.getLink("Download payment slip").click()
+        self.assertTrue(
+            'Please submit the application form before trying to download payment slips.'
+            in self.browser.contents)
+        # ... unless form is submitted.
+        self.browser.open(applicant_path + '/edit')
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='myphoto.jpg')
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        self.browser.open(applicant_path + '/' + payment_id)
+        self.browser.getLink("Download payment slip").click()
+        self.assertEqual(self.browser.headers['Content-Type'],
+                 'application/pdf')
+        return
+
+    def test_final_submit(self):
+        # Make sure that a correctly filled form with passport picture
+        # can be submitted (only) after payment
+        self.login()
+        self.browser.getLink("Edit application record").click()
+        self.assertFalse('Finally Submit' in self.browser.contents)
+        IWorkflowInfo(self.applicant).fireTransition('pay')
+        self.browser.open(self.edit_path)
+        self.assertTrue('Finally Submit' in self.browser.contents)
+        self.fill_correct_values() # fill other fields with correct values
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Finally Submit").click()
+        # We forgot to upload a passport picture
+        self.assertTrue(
+            'No passport picture uploaded' in self.browser.contents)
+        # Use a real image file and select it to be uploaded in form
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='myphoto.jpg')
+        self.browser.getControl("Finally Submit").click() # (finally) submit form
+        # The picture has been uploaded but the form cannot be submitted
+        # since the passport confirmation box was not ticked
+        self.assertTrue(
+            'Passport picture confirmation box not ticked'
+            in self.browser.contents)
+        self.browser.getControl(name="confirm_passport").value = True
+        # If application period has expired and strict-deadline is set
+        # applicants do notsee edit button and can't open
+        # the edit form.
+        self.applicantscontainer.enddate = datetime.now(pytz.utc)
+        self.browser.open(self.view_path)
+        self.assertFalse(
+            'Edit application record' in self.browser.contents)
+        self.browser.open(self.edit_path)
+        self.assertTrue(
+            'form is locked' in self.browser.contents)
+        # We can either postpone the enddate ...
+        self.applicantscontainer.enddate = datetime.now(
+            pytz.utc) + timedelta(days=10)
+        self.browser.open(self.edit_path)
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        self.assertTrue(
+            'Application submitted' in self.browser.contents)
+        # ... or allow submission after deadline.
+        IWorkflowState(self.applicant).setState('paid')
+        self.applicant.locked = False
+        self.applicantscontainer.strict_deadline = False
+        self.browser.open(self.edit_path)
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        self.assertTrue(
+            'Application submitted' in self.browser.contents)
+        return
+
+    def test_locking(self):
+        # Make sure that locked forms can't be submitted
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        # Create a pseudo image file and select it to be uploaded in form
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click()
+        # Now we lock the form
+        self.applicant.locked = True
+        self.browser.open(self.edit_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
+        return
+
+    def test_certificate_removed(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        self.browser.open(self.view_path)
+        self.assertTrue(
+            'Unnamed Certificate' in self.browser.contents)
+        self.browser.open(self.edit_path)
+        self.assertTrue(
+            '<option selected="selected" value="CERT1">' in self.browser.contents)
+        # Now we remove the certificate
+        del self.app['faculties']['fac1']['dep1'].certificates['CERT1']
+        # The certificate is still shown in display mode
+        self.browser.open(self.view_path)
+        self.assertTrue(
+            'Unnamed Certificate' in self.browser.contents)
+        # The certificate is still selectable in edit mode so that it won't
+        # be automatically replaced by another (arbitrary) certificate
+        self.browser.open(self.edit_path)
+        self.assertTrue(
+            '<option selected="selected" value="CERT1">' in self.browser.contents)
+        # Consequently, the certificate is still shown after saving the form
+        self.browser.getControl("Save").click()
+        self.browser.open(self.view_path)
+        self.assertTrue(
+            'Unnamed Certificate' in self.browser.contents)
+        # Even if we add a new certificate the previous (removed)
+        # certificate is shown
+        certificate = createObject('waeup.Certificate')
+        certificate.code = 'CERT2'
+        certificate.title = 'New Certificate'
+        certificate.application_category = 'basic'
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            certificate)
+        self.browser.open(self.edit_path)
+        self.assertTrue(
+            '<option selected="selected" value="CERT1">'
+            in self.browser.contents)
+
+    def test_upload_testfile(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values       
+        # Create a pseudo file with acceptable size
+        pdf_content = 'A' * 1024 * 300  # A string of 300 KB size
+        pseudo_pdf = StringIO(pdf_content)
+        ctrl = self.browser.getControl(name='testfile.pdf')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_pdf, filename='testfile.pdf')
+        self.browser.getControl("Save").click() # submit form
+        self.assertTrue('Uploaded file is too big!'
+            in self.browser.contents)
+        pdf_content = 'A' * 1024 * 200  # A string of 300 KB size
+        pseudo_pdf = StringIO(pdf_content)
+        ctrl = self.browser.getControl(name='testfile.pdf')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_pdf, filename='testfile.pdf')
+        self.browser.getControl("Save").click() # submit form
+        # The file has been successfully uploaded
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        # There is really a file stored for the applicant
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName(
+            attr='testfile.pdf')
+        # The stored file can be fetched
+        fd = storage.getFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 204800)
+        # A file link is displayed on the edit view ...
+        self.browser.open(self.edit_path)
+        self.assertTrue('<a href="testfile.pdf">' in self.browser.contents)
+        # ... and on the dislay view
+        self.browser.open(self.view_path)
+        self.assertTrue('testfile.pdf">Test File</a>'
+            in self.browser.contents)
+        # Adding file is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            '%s - applicants.browser.ApplicantEditFormPage'
+            ' - %s - saved: testfile.pdf'
+            % (self.applicant.applicant_id, self.applicant.applicant_id)
+            in logcontent)
+        # When an applicant is removed, also the pdf files are gone.
+        del self.applicantscontainer[self.applicant.application_number]
+        fd = storage.getFile(file_id)
+        self.assertTrue(fd is None)
+        return
+
+
+class ApplicantRegisterTests(ApplicantsFullSetup):
+    # Tests for applicant registration
+
+    layer = FunctionalLayer
+
+    def test_register_applicant_create(self):
+        config = grok.getSite()['configuration']
+        config.maintmode_enabled_by = u'any_user'
+        self.assertEqual(len(self.app['applicants'][container_name_1]), 1)
+        # An applicant can register himself.
+        self.browser.open(self.container_path)
+        self.browser.getLink("Register for application").click()
+        self.assertTrue(
+            'The portal is in maintenance mode' in self.browser.contents)
+        config.maintmode_enabled_by = None
+        self.browser.getLink("Register for application").click()
+        # The edit form now opens and can be filled with suitable values
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.lastname").value = 'Kurios'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl(name="form.phone.country").value = ['+234']
+        self.browser.getControl(name="form.phone.area").value = '555'
+        self.browser.getControl(name="form.phone.ext").value = '6666666'
+        self.browser.getControl("Send login credentials").click()
+        self.assertEqual(self.browser.url,
+            self.container_path + '/registration_complete?email=xx%40yy.zz')
+        # A new applicant has been created
+        self.assertEqual(len(self.app['applicants'][container_name_1]), 2)
+        # The new applicant can be found in the catalog via the email address
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(
+            cat.searchResults(email=('xx@yy.zz', 'xx@yy.zz')))
+        applicant = results[0]
+        self.assertEqual(applicant.lastname,'Kurios')
+        # The application_id has been copied to the reg_number
+        #self.assertEqual(applicant.applicant_id, applicant.reg_number)
+        # The applicant can be found in the catalog via the reg_number
+        #results = list(
+        #    cat.searchResults(
+        #    reg_number=(applicant.reg_number, applicant.reg_number)))
+        #self.assertEqual(applicant,results[0])
+        return
+
+    def test_register_applicant_take_unused_record(self):
+        # Create an unused record
+        uu_applicant = createObject('waeup.Applicant')
+        self.app['applicants'][container_name_1].addApplicant(uu_applicant)
+        self.assertEqual(uu_applicant.container_code, container_name_1 + '-')
+        self.assertEqual(len(self.app['applicants'][container_name_1]), 2)
+        self.browser.open(self.container_path)
+        self.browser.getLink("Register for application").click()
+        # Fill the edit form with suitable values
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        self.browser.getControl(name="form.lastname").value = 'Kurios'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl(name="form.phone.country").value = ['+234']
+        self.browser.getControl(name="form.phone.area").value = '555'
+        self.browser.getControl(name="form.phone.ext").value = '6666666'
+        self.browser.getControl("Send login credentials").click()
+        # No applicant has been created ...
+        self.assertEqual(len(self.app['applicants'][container_name_1]), 2)
+        # ... and the existing, formerly unused record has been used instead
+        self.assertEqual(uu_applicant.lastname, 'Kurios')
+        self.assertEqual(uu_applicant.container_code, container_name_1 + '+')
+        return
+
+    def test_register_applicant_update(self):
+        # We change the application mode and check if applicants
+        # can find and update imported records instead of creating new records.
+        # First we check what happens if record does not exist.
+        self.applicantscontainer.mode = 'update'
+        self.browser.open(self.container_path + '/register')
+        self.browser.getControl(name="form.lastname").value = 'Better'
+        self.browser.getControl(name="form.reg_number").value = 'anynumber'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No application record found.'
+            in self.browser.contents)
+        # Even with the correct reg_number we can't register
+        # because lastname attribute is not set.
+        self.applicantscontainer.mode = 'update'
+        self.browser.open(self.container_path + '/register')
+        self.browser.getControl(name="form.lastname").value = 'Better'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('An error occurred.' in self.browser.contents)
+        # Let's set this attribute manually
+        # and try to register with a wrong name.
+        self.applicant.lastname = u'Better'
+        self.browser.open(self.container_path + '/register')
+        self.browser.getControl(name="form.lastname").value = 'Worse'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        # Anonymous is not informed that lastname verification failed.
+        # It seems that the record doesn't exist.
+        self.assertTrue('No application record found.'
+            in self.browser.contents)
+        # Even with the correct lastname we can't register if a
+        # password has been set and used.
+        IWorkflowState(self.applicant).setState('started')
+        self.browser.getControl(name="form.lastname").value = 'Better'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your password has already been set and used.'
+            in self.browser.contents)
+        #IUserAccount(
+        #    self.app['applicants'][container_name_1][
+        #    self.applicant.application_number]).context.password = None
+        # Even without unsetting the password we can re-register if state
+        # is 'initialized'
+        IWorkflowState(self.applicant).setState('initialized')
+        self.browser.open(self.container_path + '/register')
+        # The lastname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.lastname").value = 'bEtter'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        # Yeah, we succeded ...
+        self.assertTrue('Your registration was successful.'
+            in self.browser.contents)
+        # ... and  applicant can be found in the catalog via the email address
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(
+            cat.searchResults(
+            email=('new@yy.zz', 'new@yy.zz')))
+        self.assertEqual(self.applicant,results[0])
+        return
+
+    def test_change_password_request(self):
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.identifier").value = '1234'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No record found' in self.browser.contents)
+        self.applicant.email = 'aa@aa.ng'
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.identifier").value = '1234'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue(
+            'An email with your user name and password has been sent'
+            in self.browser.contents)
+
+    def test_check_status(self):
+        self.applicant.lastname = u'Lion '
+        self.browser.open('http://localhost/app/applicants/checkstatus')
+        self.browser.getControl(name="unique_id").value = 'nonsense'
+        self.browser.getControl(name="lastname").value = 'Lion'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('No application record found' in self.browser.contents)
+        self.browser.getControl(
+          name="unique_id").value = self.applicant.applicant_id
+        self.browser.getControl(name="lastname").value = 'nonsense'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('No application record found' in self.browser.contents)
+        self.browser.getControl(
+          name="unique_id").value = self.applicant.applicant_id
+        self.browser.getControl(name="lastname").value = 'Lion'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Admission status of' in self.browser.contents)
+        self.assertTrue(
+          'You have not yet submitted your application' in self.browser.contents)
+        IWorkflowState(self.applicant).setState('admitted')
+        self.browser.open('http://localhost/app/applicants/checkstatus')
+        self.browser.getControl(
+          name="unique_id").value = self.applicant.applicant_id
+        # Whitespaces are ignored.
+        self.browser.getControl(name="lastname").value = 'Lion'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Congratulations!' in self.browser.contents)
+        self.assertFalse('Study Course' in self.browser.contents)
+        self.applicant.course_admitted = self.certificate
+        self.browser.open('http://localhost/app/applicants/checkstatus')
+        self.browser.getControl(
+          name="unique_id").value = self.applicant.applicant_id
+        self.browser.getControl(name="lastname").value = 'Lion'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Congratulations!' in self.browser.contents)
+        self.assertTrue('Unnamed Certificate (CERT1)' in self.browser.contents)
+        self.assertTrue(
+          'Department of Unnamed Department (dep1)' in self.browser.contents)
+        self.assertTrue(
+          'Faculty of Unnamed Faculty (NA)' in self.browser.contents)
+        # Also the reg_number can be used and page shows student id and password
+        # if applicant is in state created.
+        IWorkflowState(self.applicant).setState('created')
+        self.applicant.student_id = u'my id'
+        self.browser.open('http://localhost/app/applicants/checkstatus')
+        self.browser.getControl(
+          name="unique_id").value = self.applicant.reg_number
+        self.browser.getControl(name="lastname").value = 'Lion'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Congratulations!' in self.browser.contents)
+        self.assertTrue('Unnamed Certificate (CERT1)' in self.browser.contents)
+        self.assertTrue('Department of Unnamed Department (dep1)'
+            in self.browser.contents)
+        self.assertTrue(
+          'Faculty of Unnamed Faculty (NA)' in self.browser.contents)
+        self.assertTrue('user name (= student id) is: <strong>my id</strong>'
+            in self.browser.contents)
+        self.assertTrue(
+          'password is: <strong>%s</strong>' % self.applicant.application_number
+          in self.browser.contents)
+        
+
+class ApplicantsExportTests(ApplicantsFullSetup, FunctionalAsyncTestCase):
+    # Tests for StudentsContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def wait_for_export_jobs_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_ids = [i[0] for i in self.app['datacenter'].running_exports]
+        jobs = [manager.get(job_id) for job_id in job_ids]
+        for job in jobs:
+            wait_for_result(job)
+        return job_ids
+
+    def test_applicants_in_container_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        container_path = 'http://localhost/app/applicants/%s' % container_name_1
+        self.browser.open(container_path)
+        self.browser.getLink("Export application data").click()
+        self.browser.getControl("Start new exports").click()
+        job_ids = self.wait_for_export_jobs_completed()
+        # Two exports were created
+        self.assertEqual(len(self.app['datacenter'].running_exports), 2)
+       # When the jobs are finished and we reload the page...
+        self.browser.open(container_path + '/exports')
+        # ... the both csv files can be downloaded ...
+        self.browser.getLink("Download", index=0).click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_applicants_%s.csv' % job_ids[0] in
+            self.browser.headers['content-disposition'])
+        self.browser.open(container_path + '/exports')
+        self.browser.getLink("Download", index=1).click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_applicantpayments_%s.csv' % job_ids[1] in
+            self.browser.headers['content-disposition'])
+        # ... and discarded
+        self.browser.open(container_path + '/exports')
+        self.browser.getControl("Discard", index=0).click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding are logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerJobStart - '
+            'exported: applicants (%s), job_id=%s'
+            % (container_name_1, job_ids[0]) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_applicants_%s.csv, job_id=%s'
+            % (job_ids[0], job_ids[0]) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_ids[0] in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerJobStart - '
+            'exported: applicantpayments (%s), job_id=%s'
+            % (container_name_1, job_ids[1]) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_applicantpayments_%s.csv, job_id=%s'
+            % (job_ids[1], job_ids[1]) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_ids[1] in logcontent
+            )
+
+class ApplicantRefereeReportTests(ApplicantsFullSetup, FunctionalAsyncTestCase):
+    # Tests for ApplicantRefereeReport class views and pages
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantRefereeReportTests, self).setUp()
+        self.setup_logging()
+        return
+
+    def tearDown(self):
+        super(ApplicantRefereeReportTests, self).tearDown()
+        self.teardown_logging()
+        return
+
+    def setup_logging(self):
+        # setup a log-handler that catches all fake mailer output
+        self.stream = StringIO()
+        handler = logging.StreamHandler(self.stream)
+        logger = logging.getLogger('test.smtp')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return
+
+    def get_fake_smtp_output(self):
+        # get output generated by fake mailer
+        self.stream.flush()
+        self.stream.seek(0)
+        return self.stream.read()
+
+    def teardown_logging(self):
+        # remove the log handler for fake mailer output
+        logger = logging.getLogger('test.smtp')
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            logger.removeHandler(handler)
+        return
+
+    def test_refereereport_mandate(self):
+        mandate = RefereeReportMandate()
+        mandate.params['name'] = u'John Referee'
+        mandate.params['email'] = 'aa@aa.aa'
+        mandate.params[
+            'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
+                container_name_1, self.applicant.application_number)
+        self.app['mandates'].addMandate(mandate)
+        # Let's open the add form page via the mandate view
+        self.browser.open('http://localhost/app/mandate?mandate_id=%s'
+            % mandate.mandate_id)
+        # Form page opens and is prefilled
+        self.assertEqual(
+            self.browser.url,
+            'http://localhost/app/applicants/%s/%s/addrefereereport?mandate_id=%s'
+            % (container_name_1, self.applicant.application_number,
+            mandate.mandate_id))
+        self.assertTrue('value="John Referee"' in self.browser.contents)
+        # Let's open the page directly with an invalid mandate
+        self.browser.open(
+            'http://localhost/app/applicants/%s/%s/addrefereereport?mandate_id=wrongmadate'
+            % (container_name_1, self.applicant.application_number))
+        self.assertTrue('<div class="alert alert-warning">No mandate.</div>'
+            in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+        # Page is also blocked in maintenance mode
+        grok.getSite()['configuration'].maintmode_enabled_by = u'anybody'
+        self.browser.open(
+            'http://localhost/app/applicants/%s/%s/addrefereereport?mandate_id=%s'
+            % (container_name_1, self.applicant.application_number,
+            mandate.mandate_id))
+        self.assertTrue('<div class="alert alert-warning">The portal is '
+                        'in maintenance mode'
+            in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+
+    def test_add_and_view_reports(self):
+        mandate = RefereeReportMandate()
+        mandate.params['name'] = u'John Referee'
+        mandate.params['email'] = 'aa@aa.aa'
+        mandate.params[
+            'redirect_path'] = '/applicants/%s/%s/addrefereereport' % (
+                container_name_1, self.applicant.application_number)
+        self.app['mandates'].addMandate(mandate)
+        self.assertEqual(len(self.app['mandates'].keys()), 1)
+        # Let's open the add form page via the mandate view
+        self.browser.open('http://localhost/app/mandate?mandate_id=%s'
+            % mandate.mandate_id)
+        self.assertTrue('Joan None' in self.browser.contents)
+        # Report can't be saved without required fields
+        self.browser.getControl(name="form.email").value = ''
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Required input is missing' in self.browser.contents)
+        self.browser.getControl(name="form.email").value = 'bb@bb.bb'
+        self.browser.getControl("Submit").click()
+        self.assertTrue('Referee report has been saved' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+        # Report has been created
+        self.assertEqual(len(self.applicant.refereereports), 1)
+        report = self.applicant.refereereports[0]
+        # Managers can view the report
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_path)
+        self.browser.getLink("%s" % report.r_id).click()
+        self.assertEqual(
+            self.browser.url, self.view_path  + '/%s' % report.r_id)
+        self.assertTrue('John Referee' in self.browser.contents)
+        # Report creation is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.anybody - applicants.browser.RefereeReportAddFormPage - '
+            '%s - added: %s\n' % (self.applicant.applicant_id, report.r_id)
+            in logcontent
+            )
+        # Mandate is deleted
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+
+    def test_final_submit_with_referees(self):
+        # Add two referees
+        referee1 = RefereeEntry()
+        referee2 = RefereeEntry()
+        referee1.name = u'Linda Tree'
+        referee1.email = 'linda@forest.de'
+        referee2.name = u'Otis Stone'
+        referee2.email = 'otis@stones.de'
+        self.applicant.referees = [referee1, referee2]
+        self.assertFalse(referee1.email_sent)
+        self.assertFalse(referee2.email_sent)
+        self.login()
+        IWorkflowInfo(self.applicant).fireTransition('pay')
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='form.passport')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='myphoto.jpg')
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        mandate_id_0 = self.app['mandates'].keys()[0]
+        mandate_id_1 = self.app['mandates'].keys()[1]
+        if self.app['mandates'].values()[0].params['name'] == 'Linda Tree':
+            mandate_id_0 = self.app['mandates'].keys()[0]
+            mandate_id_1 = self.app['mandates'].keys()[1]
+        else:
+            mandate_id_0 = self.app['mandates'].keys()[1]
+            mandate_id_1 = self.app['mandates'].keys()[0]
+        self.assertMatches(
+            'Sending email from no-reply@waeup.org to linda@forest.de:'
+            '\nMessage:'
+            '\nmsg: MIME-Version: 1.0\nmsg: Content-Type: text/plain; charset="us-ascii"'
+            '\nmsg: Content-Transfer-Encoding: 7bit'
+            '\nmsg: From: Administrator <no-reply@waeup.org>'
+            '\nmsg: To: Linda Tree <linda@forest.de>'
+            '\nmsg: Reply-To: Administrator <contact@waeup.org>'
+            '\nmsg: Subject: Request for referee report from Sample University'
+            '\nmsg: '
+            '\nmsg: Dear Linda Tree,'
+            '\nmsg: '
+            '\nmsg: The candidate with Id app%s_372052 and name John Anthony Tester applied to'
+            '\nmsg: the Sample University to study Unnamed Certificate for the %s/%s session.'
+            '\nmsg: The candidate has listed you as referee. You are thus required to kindly use'
+            '\nmsg: the link below to provide your referral remarks on or before'
+            '\nmsg: 2016-08-12 08:32:41.619671+00:00.'
+            '\nmsg: '
+            '\nmsg: Report link: http://localhost/app/mandate?mandate_id=%s'
+            '\nmsg: '
+            '\nmsg: Thank You'
+            '\nmsg: '
+            '\nmsg: The Secretary'
+            '\nmsg: Post Graduate School'
+            '\nmsg: Sample University'
+            '\nmsg: '
+            '\nSending email from no-reply@waeup.org to otis@stones.de:'
+            '\nMessage:'
+            '\nmsg: MIME-Version: 1.0'
+            '\nmsg: Content-Type: text/plain; charset="us-ascii"'
+            '\nmsg: Content-Transfer-Encoding: 7bit'
+            '\nmsg: From: Administrator <no-reply@waeup.org>'
+            '\nmsg: To: Otis Stone <otis@stones.de>'
+            '\nmsg: Reply-To: Administrator <contact@waeup.org>'
+            '\nmsg: Subject: Request for referee report from Sample University'
+            '\nmsg: '
+            '\nmsg: Dear Otis Stone,'
+            '\nmsg: '
+            '\nmsg: The candidate with Id app%s_<6-DIGITS> and name John Anthony Tester applied to'
+            '\nmsg: the Sample University to study Unnamed Certificate for the %s/%s session.'
+            '\nmsg: The candidate has listed you as referee. You are thus required to kindly use'
+            '\nmsg: the link below to provide your referral remarks on or before'
+            '\nmsg: <YYYY-MM-DD hh:mm:ss>.<6-DIGITS>+00:00.'
+            '\nmsg: '
+            '\nmsg: Report link: http://localhost/app/mandate?mandate_id=%s'
+            '\nmsg: '
+            '\nmsg: Thank You'
+            '\nmsg: '
+            '\nmsg: The Secretary'
+            '\nmsg: Post Graduate School'
+            '\nmsg: Sample University'
+            '\nmsg: '
+            % (session_1, session_1, session_2, mandate_id_0,
+               session_1, session_1, session_2, mandate_id_1,),
+            self.get_fake_smtp_output()
+            )
+        self.assertTrue(
+            'Application submitted' in self.browser.contents)
+        self.assertTrue(
+            'Form has been successfully submitted and 2 '
+            'invitation emails were sent.' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            '%s - applicants.browser.ApplicantEditFormPage - %s - '
+            'email sent: otis@stones.de' %
+            (self.applicant.applicant_id, self.applicant.applicant_id)
+            in logcontent)
+        self.assertTrue(referee1.email_sent)
+        self.assertTrue(referee2.email_sent)
+        # If the form is being resubmitted, no more emails will be sent
+        IWorkflowState(self.applicant).setState('paid')
+        self.applicant.locked = False
+        self.browser.open(self.edit_path)
+        self.browser.getControl(name="confirm_passport").value = True
+        self.browser.getControl("Finally Submit").click()
+        self.assertTrue(
+            'Form has been successfully submitted and 0 '
+            'invitation emails were sent.' in self.browser.contents)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_catalog.py	(revision 16045)
@@ -0,0 +1,117 @@
+## $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
+##
+"""
+Tests for applicants specific catalog (currently only one).
+"""
+import shutil
+import tempfile
+import grok
+from zope.component import getUtility, createObject
+from hurry.query import Eq
+from hurry.query.interfaces import IQuery
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.component.interfaces import ComponentLookupError
+from zope.testbrowser.testing import Browser
+from waeup.kofa.app import University
+from waeup.kofa.applicants.container import ApplicantsContainer
+from waeup.kofa.applicants.applicant import Applicant
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+class CatalogTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CatalogTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(CatalogTests, self).tearDown()
+        shutil.rmtree(self.dc_root)
+
+    def create_applicant(self):
+        # Create an applicant in an applicants container
+        self.container = ApplicantsContainer()
+        self.container.code = u"mystuff"
+        setSite(self.app)
+        self.app['applicants']['mystuff'] = self.container
+        self.applicant = Applicant()
+        self.app['applicants']['mystuff'].addApplicant(self.applicant)
+        return
+
+    def test_get_applicants_catalog(self):
+        # There is no global applicants catalog, but one for each site.
+        self.assertRaises(
+            ComponentLookupError,
+            getUtility, ICatalog, name='applicants_catalog')
+        # If we are 'in the local site' we can get an applicants catalog.
+        setSite(self.app)
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        self.assertTrue(ICatalog.providedBy(cat))
+
+    def test_get_applicant(self):
+        # Make sure that applicants are really catalogued on creation.
+        self.create_applicant()
+        q = getUtility(IQuery)
+        subquery = Eq(('applicants_catalog', 'applicant_id'),
+            self.applicant.applicant_id)
+        results = list(q.searchResults(subquery))
+        self.assertEqual(len(results), 1)
+        result_applicant = results[0]
+        self.assertTrue(isinstance(result_applicant, Applicant))
+        self.assertEqual(result_applicant.applicant_id, self.applicant.applicant_id)
+
+    def test_get_payment(self):
+        self.create_applicant()
+        q = getUtility(IQuery)
+        subquery = Eq(('applicants_catalog', 'applicant_id'),
+            self.applicant.applicant_id)
+        results = list(q.searchResults(subquery))
+        self.assertEqual(len(results), 1)
+        result_applicant = results[0]
+        payment = createObject('waeup.ApplicantOnlinePayment')
+        payment.p_id = 'p1234567890'
+        payment.p_item = u'Payment Item'
+        payment.p_session = 2011
+        payment.p_category = 'application'
+        result_applicant[payment.p_id] = payment
+        # We can find a payment ticket by the payment session ...
+        cat = getUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_session=(2011, 2011))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is result_applicant['p1234567890']
+        # ... and by the payment id
+        results = cat.searchResults(p_id=('p1234567890', 'p1234567890'))
+        assert len(results) == 1
+        # If we remove the applicant also the payment disappears
+        del self.app['applicants']['mystuff'][result_applicant.application_number]
+        results = cat.searchResults(p_id=('p1234567890', 'p1234567890'))
+        assert len(results) == 0
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_container.py	(revision 16045)
@@ -0,0 +1,51 @@
+## $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
+##
+"""
+Tests for applicants containers.
+"""
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.applicants import interfaces
+from waeup.kofa.applicants.container import (
+    ApplicantsContainer,
+    )
+
+class ApplicantsContainerTestCase(unittest.TestCase):
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                interfaces.IApplicantsContainer, ApplicantsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                interfaces.IApplicantsContainer, ApplicantsContainer())
+            )
+        return
+
+def suite():
+    suite = unittest.TestSuite()
+    for test_case in [
+        ApplicantsContainerTestCase,
+        ]:
+        suite.addTests(
+            unittest.TestLoader().loadTestsFromTestCase(test_case))
+    return suite
+
+test_suite = suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_dynamicroles.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_dynamicroles.py	(revision 16045)
@@ -0,0 +1,85 @@
+## $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
+##
+"""
+Tests for dynamic roles concerning applicants and related.
+"""
+from zope.interface import verify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.settings import Allow
+from zope.securitypolicy.tests.test_annotationprincipalrolemanager import (
+    Test as APRMTest, Manageable)
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.applicants import ApplicantPrincipalRoleManager
+
+class ApplicantPrincipalRoleManagerTests(APRMTest):
+    # Make sure our PRM behaves like a regular one for usual cases.
+    # Usual cases are ones, that do not interact with external officers.
+    # See next test case for functional tests including officer perms.
+
+    def _make_roleManager(self, obj=None):
+        # Overriding this method of original testcase we make sure
+        # that really an Applicant PRM is tested.
+        if obj is None:
+            obj = Manageable()
+        return ApplicantPrincipalRoleManager(obj)
+
+class ApplicantPrincipalRoleManagerFunctionalTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantPrincipalRoleManagerFunctionalTests, self).setUp()
+        self.applicant.course1 = self.certificate
+        # assign application manage permissions for a virtual officer
+        prm = IPrincipalRoleManager(self.department)
+        prm.assignRoleToPrincipal('waeup.local.ApplicationsManager', 'alice')
+        return
+
+    def test_iface(self):
+        # make sure out ApplicantPRM really implements required ifaces.
+        obj = ApplicantPrincipalRoleManager(self.applicant)
+        verify.verifyClass(IPrincipalRoleManager,
+                           ApplicantPrincipalRoleManager)
+        verify.verifyObject(IPrincipalRoleManager, obj)
+        return
+
+    def test_get_as_adapter(self):
+        # we can get an ApplicantPRM for Applicants by adapter lookup
+        prm = IPrincipalRoleManager(self.applicant)
+        self.assertTrue(
+            isinstance(prm, ApplicantPrincipalRoleManager))
+        return
+
+    def test_no_officer_set(self):
+        # if the faculty/dept. of the connected cert has no local
+        # roles set, we won't get any additional roles for our
+        # applicant
+        prm = IPrincipalRoleManager(self.applicant)
+        result = prm.getRolesForPrincipal('bob')
+        self.assertEqual(result, [])
+        return
+
+    def test_valid_officer(self):
+        # for an officer that has application manage permissions
+        # on the connected dept
+        # we get the ApplicationsManager role on our applicant
+        prm = IPrincipalRoleManager(self.applicant)
+        result = prm.getRolesForPrincipal('alice')
+        self.assertEqual(result, [('waeup.ApplicationsManager', Allow)])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_export.py	(revision 16045)
@@ -0,0 +1,324 @@
+import datetime
+import os
+import pytz
+import shutil
+import tempfile
+import unittest
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.interface.verify import verifyObject, verifyClass
+from zope.intid.interfaces import IIntIds
+from waeup.kofa.applicants import ApplicantsContainer
+from waeup.kofa.applicants.export import (
+    ApplicantsContainerExporter, ApplicantExporter,
+    ApplicantPaymentExporter)
+from waeup.kofa.applicants.interfaces import (
+    AppCatSource, ApplicationTypeSource)
+from waeup.kofa.applicants.tests.test_batching import (
+    ApplicantImportExportSetup)
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.schoolgrades import ResultEntry
+from waeup.kofa.testing import KofaUnitTestLayer, FunctionalLayer
+from waeup.kofa.utils.utils import KofaUtils
+
+class ApplicantsContainersExporterTest(unittest.TestCase):
+
+    layer = KofaUnitTestLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = ApplicantsContainerExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, ApplicantsContainerExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a faculty exporter as utility
+        result = queryUtility(ICSVExporter, name="applicantscontainers")
+        self.assertTrue(result is not None)
+        return
+
+    def setup_container(self, container):
+        # set all attributes of a container
+        container.code = u'dp2017'
+        container.title = u'General Studies'
+        container.prefix = list(ApplicationTypeSource()(container))[0]
+        container.year = 2017
+        container.application_category = list(AppCatSource()(container))[0]
+        container.description = u'Some Description\nwith linebreak\n'
+        container.description += u'<<de>>man spriht deutsh'
+        container.startdate = datetime.datetime(
+            2017, 1, 1, 12, 0, 0, tzinfo=pytz.utc)
+        container.enddate = datetime.datetime(
+            2017, 1, 31, 23, 0, 0, tzinfo=pytz.utc)
+        return container
+
+    def test_export(self):
+        # we can export a set of applicants containers (w/o applicants)
+        container = ApplicantsContainer()
+        container = self.setup_container(container)
+        exporter = ApplicantsContainerExporter()
+        exporter.export([container], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'application_category,application_fee,application_slip_notice,code,'
+            'description,enddate,hidden,mode,prefix,startdate,'
+            'strict_deadline,title,with_picture,year\r\n'
+
+            'basic,0.0,,dp2017,'
+            '"Some Description\nwith linebreak\n<<de>>man spriht deutsh",'
+            '2017-01-31 23:00:00+00:00#,0,,app,2017-01-01 12:00:00+00:00#,1,'
+            'General Studies,1,2017\r\n'
+            )
+        return
+
+class ApplicantExporterTest(ApplicantImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantExporterTest, self).setUp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        self.cat = getUtility(ICatalog, name='applicants_catalog')
+        self.intids = getUtility(IIntIds)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = ApplicantExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, ApplicantExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an applicant exporter as utility
+        result = queryUtility(ICSVExporter, name="applicants")
+        self.assertTrue(result is not None)
+        return
+
+    def setup_applicant(self, applicant):
+        # set predictable values for `applicant`
+        applicant.reg_number = u'123456'
+        applicant.applicant_id = u'dp2011_654321'
+        applicant.firstname = u'Anna'
+        applicant.lastname = u'Tester'
+        applicant.middlename = u'M.'
+        applicant.date_of_birth = datetime.date(1981, 2, 4)
+        applicant.sex = 'f'
+        applicant.email = 'anna@sample.com'
+        applicant.phone = u'+234-123-12345'
+        applicant.course1 = self.certificate
+        applicant.course2 = self.certificate
+        applicant.course_admitted = self.certificate
+        applicant.notice = u'Some notice\nin lines.'
+        applicant.password = 'any password'
+        result_entry = ResultEntry(
+            KofaUtils.EXAM_SUBJECTS_DICT.keys()[0],
+            KofaUtils.EXAM_GRADES[0][0]
+            )
+        applicant.school_grades = [
+            result_entry]
+        return applicant
+
+    def test_export_emtpy(self):
+        # we can export nearly empty applicants
+        self.applicant.applicant_id = u'dp2011_654321'
+        exporter = ApplicantExporter()
+        exporter.export([self.applicant], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        # The exported records do contain a real date in their
+        # history dict. We skip the date and split the comparison
+        # into two parts.
+        self.assertTrue(
+            'applicant_id,course1,course2,course_admitted,date_of_birth,'
+            'email,firstname,lastname,locked,middlename,notice,phone,'
+            'reg_number,sex,special_application,student_id,suspended,'
+            'password,state,history,container_code,application_number,'
+            'display_fullname,application_date\r\n'
+            'dp2011_654321,,,,,,Anna,Tester,0'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],dp2011,654321,Anna Tester'
+            in result)
+        return
+
+    def test_export(self):
+        # we can really export applicants
+        # set values we can expect in export file
+        applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantExporter()
+        exporter.export([applicant], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        # The exported records do contain a real date in their
+        # history dict. We skip the date and split the comparison
+        # into two parts.
+        self.assertTrue(
+            'applicant_id,course1,course2,course_admitted,date_of_birth,'
+            'email,firstname,lastname,locked,middlename,notice,phone,'
+            'reg_number,sex,special_application,student_id,suspended,'
+            'password,state,history,container_code,application_number,'
+            'display_fullname,application_date\r\n'
+            'dp2011_654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'anna@sample.com,Anna,Tester,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],dp2011,654321,'
+            'Anna M. Tester,\r\n'
+            in result)
+
+        return
+
+    def test_export_all(self):
+        # we can export all applicants in a portal
+        # set values we can expect in export file
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'applicant_id,course1,course2,course_admitted,date_of_birth,'
+            'email,firstname,lastname,locked,middlename,notice,phone,'
+            'reg_number,sex,special_application,student_id,suspended,'
+            'password,state,history,container_code,application_number,'
+            'display_fullname,application_date\r\n'
+            'dp2011_654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'anna@sample.com,Anna,Tester,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],dp2011,654321,'
+            'Anna M. Tester,\r\n'
+            in result)
+        return
+
+    def test_export_filtered(self):
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantExporter()
+        exporter.export_filtered(
+            self.app, self.outfile, container=self.container.code)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'applicant_id,course1,course2,course_admitted,date_of_birth,'
+            'email,firstname,lastname,locked,middlename,notice,phone,'
+            'reg_number,sex,special_application,student_id,suspended,'
+            'password,state,history,container_code,application_number,'
+            'display_fullname,application_date\r\n'
+            'dp2011_654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'anna@sample.com,Anna,Tester,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],dp2011,654321,'
+            'Anna M. Tester,\r\n'
+            in result)
+        # In empty container no applicants are exported
+        container = ApplicantsContainer()
+        container.code = u'anything'
+        self.app['applicants']['anything'] = self.container
+        exporter.export_filtered(
+            self.app, self.outfile, container=container.code)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'applicant_id,course1,course2,course_admitted,date_of_birth,'
+            'email,firstname,lastname,locked,middlename,notice,phone,'
+            'reg_number,sex,special_application,student_id,suspended,'
+            'password,state,history,container_code,application_number,'
+            'display_fullname,application_date\r\n'
+            in result)
+        return
+
+
+class ApplicantPaymentExporterTest(ApplicantImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantPaymentExporterTest, self).setUp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        self.cat = getUtility(ICatalog, name='applicants_catalog')
+        self.intids = getUtility(IIntIds)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = ApplicantPaymentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, ApplicantPaymentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an applicant exporter as utility
+        result = queryUtility(ICSVExporter, name="applicantpayments")
+        self.assertTrue(result is not None)
+        return
+
+    def setup_applicant(self, applicant):
+        # set predictable values for `applicant`
+        applicant.reg_number = u'123456'
+        applicant.applicant_id = u'dp2011_654321'
+        applicant.firstname = u'Anna'
+        applicant.lastname = u'Tester'
+        applicant.middlename = u'M.'
+        applicant.date_of_birth = datetime.date(1981, 2, 4)
+        applicant.sex = 'f'
+        applicant.email = 'anna@sample.com'
+        applicant.phone = u'+234-123-12345'
+        # Add payment
+        payment = createObject(u'waeup.ApplicantOnlinePayment')
+        payment.p_id = 'p120'
+        payment.p_session = 2012
+        payment.p_category = 'application'
+        payment.p_state = 'paid'
+        applicant['p120'] = payment
+        return applicant
+
+    def test_export(self):
+        applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantPaymentExporter()
+        exporter.export(applicant.payments, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        cdate = str('%s#' % self.applicant['p120'].creation_date)
+        self.assertEqual(
+            'ac,amount_auth,creation_date,p_category,p_combi,p_id,'
+            'p_item,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,applicant_id,reg_number,display_fullname\r\n'
+            ',0.0,%s,application,[],p120,,2012,paid,,0.0,,,dp2011_654321,'
+            '123456,Anna M. Tester\r\n' % cdate, result)
+        return
+
+    def test_export_all(self):
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantPaymentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        cdate = str('%s#' % self.applicant['p120'].creation_date)
+        self.assertEqual(
+            'ac,amount_auth,creation_date,p_category,p_combi,p_id,'
+            'p_item,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,applicant_id,reg_number,display_fullname\r\n'
+            ',0.0,%s,application,[],p120,,2012,paid,,0.0,,,dp2011_654321,'
+            '123456,Anna M. Tester\r\n' % cdate, result)
+        return
+
+    def test_export_filtered(self):
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantPaymentExporter()
+        exporter.export_filtered(
+            self.app, self.outfile, container=self.container.code)
+        result = open(self.outfile, 'rb').read()
+        cdate = str('%s#' % self.applicant['p120'].creation_date)
+        self.assertEqual(
+            'ac,amount_auth,creation_date,p_category,p_combi,p_id,'
+            'p_item,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,applicant_id,reg_number,display_fullname\r\n'
+            ',0.0,%s,application,[],p120,,2012,paid,,0.0,,,dp2011_654321,'
+            '123456,Anna M. Tester\r\n' % cdate, result)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_interfaces.py	(revision 16045)
@@ -0,0 +1,68 @@
+## $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 components defined in interface module.
+"""
+import unittest
+from zc.sourcefactory.browser.source import FactoredTerms
+from zope.publisher.browser import TestRequest
+from waeup.kofa.students.vocabularies import GenderSource
+
+class InterfacesTest(unittest.TestCase):
+
+    def setUp(self):
+        self.source = GenderSource()
+        self.request = TestRequest()
+        self.terms = FactoredTerms(self.source, self.request)
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_GenderSource_list(self):
+        result = list(self.source)
+        self.assertEqual(result, ['m', 'f'])
+
+    def test_GenderSource_term_male(self):
+        term = self.terms.getTerm('m')
+        assert term.title == 'male'
+        assert term.token == 'm'
+        assert term.value == 'm'
+
+    def test_GenderSource_term_female(self):
+        term = self.terms.getTerm('f')
+        assert term.title == 'female'
+        assert term.token == 'f'
+        assert term.value == 'f'
+
+    def test_GernderSource_term_invalid(self):
+        term_inv = self.terms.getTerm('Invalid')
+        assert term_inv.title is None
+        assert term_inv.token == 'i'
+
+def suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+            InterfacesTest,
+            ]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
+    return suite
+
+test_suite = suite
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_payment.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_payment.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_payment.py	(revision 16045)
@@ -0,0 +1,42 @@
+## $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
+##
+"""Tests for application payments.
+
+"""
+import unittest
+from zope.interface import verify
+from waeup.kofa.applicants.payment import (
+    ApplicantOnlinePayment, ApplicantOnlinePaymentFactory, Payer)
+from waeup.kofa.payments.interfaces import IOnlinePayment, IPayer
+
+class ApplicantOnlinePaymentFactoryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.factory = ApplicantOnlinePaymentFactory()
+        return
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, ApplicantOnlinePayment)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IOnlinePayment)
+
+    def test_payer_interface(self):
+        verify.verifyClass(IPayer, Payer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_pdf.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_pdf.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_pdf.py	(revision 16045)
@@ -0,0 +1,61 @@
+## $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 PDF components.
+"""
+import unittest
+
+from zope.component import getAdapter
+from zope.interface import verify
+from waeup.kofa.applicants.pdf import IPDF, PDFApplicationSlip
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.kofa.interfaces import IPDF
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+
+class PDFApplicationSlipTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement/provide the promised interfaces
+        obj = PDFApplicationSlip(self.applicant)
+        verify.verifyClass(IPDF, PDFApplicationSlip)
+        verify.verifyObject(IPDF, obj)
+        return
+
+    def test_adapter(self):
+        # we can get PDFs by adapter lookup
+        pdfcreator = getAdapter(self.applicant, IPDF, name='application_slip')
+        self.assertTrue(pdfcreator is not None)
+        return
+
+    def test_call(self):
+        # we can generate PDFs from applicants
+        pdfcreator = PDFApplicationSlip(self.applicant)
+        result = pdfcreator()
+        self.assertTrue(result.startswith('%PDF-1.4'))
+        return
+
+    def test_call_with_admission_set(self):
+        # we can generate PDF if admission course is set.
+        # XXX: implement test
+        pass
+
+    def test_call_with_view(self):
+        # we can provide extra info if we have a view
+        # XXX: implement test
+        pass
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_refereereport.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_refereereport.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_refereereport.py	(revision 16045)
@@ -0,0 +1,77 @@
+## $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
+##
+"""Tests for application payments.
+
+"""
+import unittest
+from datetime import datetime
+from zope.interface import verify
+from zope.component import createObject
+from waeup.kofa.applicants.refereereport import (
+    ApplicantRefereeReport, ApplicantRefereeReportFactory)
+from waeup.kofa.applicants.interfaces import IApplicantRefereeReport
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+
+
+class ApplicantRefereeReportTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantRefereeReportTest, self).setUp()
+        self.report = createObject(u'waeup.ApplicantRefereeReport')
+        self.report.r_id = 'my_report'
+        self.applicant = createObject(u'waeup.Applicant')
+        self.applicant[self.report.r_id] = self.report
+        return
+
+    def tearDown(self):
+        super(ApplicantRefereeReportTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verify.verifyClass(
+                IApplicantRefereeReport, ApplicantRefereeReport)
+            )
+        self.assertTrue(
+            verify.verifyObject(
+                IApplicantRefereeReport, ApplicantRefereeReport())
+            )
+        return
+
+    def test_report(self):
+        self.assertEqual(self.applicant['my_report'], self.report)
+        self.assertTrue(isinstance(self.report.creation_date, datetime))
+        return
+
+
+class ApplicantRefereeReportFactoryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.factory = ApplicantRefereeReportFactory()
+        return
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, ApplicantRefereeReport)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IApplicantRefereeReport)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_root.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_root.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_root.py	(revision 16045)
@@ -0,0 +1,203 @@
+## $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 applicants root.
+"""
+import logging
+import shutil
+import tempfile
+import unittest
+from StringIO import StringIO
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.app import University
+from waeup.kofa.applicants import (
+    interfaces, Applicant, ApplicantsContainer,
+    )
+from waeup.kofa.applicants.root import (
+    ApplicantsRoot, ApplicantsPlugin,
+    )
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger)
+
+
+class FakeSite(dict):
+    pass
+
+class ApplicantsRootTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        remove_logger('waeup.kofa.app.applicants')
+        super(ApplicantsRootTestCase, self).setUp()
+        # Setup a sample site for each test
+        # Prepopulate the ZODB...
+        app = University()
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+        return
+
+    def tearDown(self):
+        super(ApplicantsRootTestCase, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                interfaces.IApplicantsRoot, ApplicantsRoot)
+            )
+        self.assertTrue(
+            verifyObject(
+                interfaces.IApplicantsRoot, ApplicantsRoot())
+            )
+        return
+
+    def test_logger(self):
+        # We can get a logger from root
+        logger = self.app['applicants'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.kofa.app.applicants'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('applicants.log')
+        assert filename.startswith(self.dc_root)
+
+    def test_logger_multiple(self):
+        # Make sure the logger is still working after 2nd call
+        # First time we call it, it might be registered
+        logger = self.app['applicants'].logger
+        # At second call the already registered logger should be returned
+        logger = self.app['applicants'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.kofa.app.applicants'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('applicants.log')
+        assert filename.startswith(self.dc_root)
+
+class ApplicantsRootPluginTestCase(unittest.TestCase):
+    def create_logger(self):
+        # create a logger suitable for local tests.
+        test_logger = logging.getLogger('waeup.kofa.applicants.testlogger')
+        log = StringIO()
+        handler = logging.StreamHandler(log)
+        handler.setLevel(logging.DEBUG)
+        test_logger.addHandler(handler)
+        test_logger.setLevel(logging.DEBUG)
+        self.logger = test_logger
+        self.log = log
+        self.handler = handler
+        return self.logger
+
+    def remove_logger(self):
+        del self.handler
+        del self.logger
+        del self.log
+        pass
+
+    def get_log(self):
+        self.log.seek(0)
+        return self.log.read()
+
+    def setUp(self):
+        self.create_logger()
+        return
+
+    def tearDown(self):
+        self.remove_logger()
+        return
+
+    # Real tests start here...
+    def test_pluginsetup(self):
+        # Make sure we can add ApplicantsRoot to sites.
+        site = FakeSite()
+        plugin = ApplicantsPlugin()
+        plugin.setup(site, 'blah', self.logger)
+        self.assertTrue('applicants' in site.keys())
+        log = self.get_log()
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_new(self):
+        # Run update on a site without applicants root.
+        site = FakeSite()
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue('applicants' in site.keys())
+        log = self.get_log()
+        self.assertTrue('Updating site at <Unnamed Site>' in log)
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_outdated(self):
+        # Run update on a site with outdated applicants root.
+        site = FakeSite()
+        root = object() # # This is not a proper applicants root
+        site['applicants'] = root
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue(site['applicants'] is not root)
+        self.assertTrue(isinstance(site['applicants'], ApplicantsRoot))
+        log = self.get_log()
+        self.assertTrue('Outdated applicants folder detected' in log)
+        self.assertTrue('Updating site at <Unnamed Site>' in log)
+        self.assertTrue('Installed applicants root.' in log)
+        return
+
+    def test_update_uptodate(self):
+        # Run update on a site with proper applicants root.
+        site = FakeSite()
+        root = ApplicantsRoot()
+        site['applicants'] = root
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        self.assertTrue(site['applicants'] is root)
+        log = self.get_log()
+        self.assertTrue('Updating site at <Unnamed Site>' in log)
+        self.assertTrue('Nothing to do' in log)
+        return
+
+    def test_update_log(self):
+        # Check that sitename is used in log messages on updates.
+        site = FakeSite()
+        site.__name__ = 'my_site'
+        plugin = ApplicantsPlugin()
+        plugin.update(site, 'blah', self.logger)
+        log = self.get_log()
+        self.assertTrue('Updating site at my_site.' in log)
+        return
+
+def suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+            ApplicantsRootTestCase,
+            ApplicantsRootPluginTestCase,
+            ]:
+        suite.addTests(unittest.makeSuite(testcase))
+    return suite
+
+test_suite = suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_webservices.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_webservices.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/tests/test_webservices.py	(revision 16045)
@@ -0,0 +1,56 @@
+# Tests for applicant webservices
+from zope.app.testing.xmlrpc import ServerProxy
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+
+
+class XMLRPCTests(ApplicantsFullSetup):
+    # check XMLRPC services for university portal
+
+    layer = FunctionalLayer
+
+    def setup_applicant(self, applicant):
+        applicant.firstname = u'Bob'
+        applicant.lastname = u'Bubble'
+        applicant.email = 'bob@sample.com'
+        pass
+
+    def test_check_applicant_credentials_valid(self):
+        # make sure we can get applicant infos providing valid creds
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_applicant(self.applicant)
+        apl_id = self.applicant.applicant_id
+        result = server.check_applicant_credentials(apl_id, 'apwd')
+        self.assertEqual(
+            result, {
+                'description': 'Bob Bubble',
+                'email': 'bob@sample.com',
+                'id': apl_id,
+                'type': 'applicant'}
+            )
+        return
+
+    def test_check_applicant_credentials_invalid(self):
+        # invalid creds will give no results
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_applicant(self.applicant)
+        apl_id = self.applicant.applicant_id
+        result = server.check_applicant_credentials(apl_id, 'wrong-pw')
+        self.assertEqual(result, None)
+        return
+
+    def test_get_moodle_data(self):
+        # make sure we can get applicant infos providing valid creds
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_applicant(self.applicant)
+        apl_id = self.applicant.applicant_id
+        result = server.get_applicant_moodle_data(apl_id)
+        self.assertEqual(
+            result, {
+                'lastname': 'Bubble',
+                'email': 'bob@sample.com',
+                'firstname': 'Bob',
+                }
+            )
+        return
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/utils.py	(revision 16045)
@@ -0,0 +1,119 @@
+## $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
+##
+"""General helper functions and utilities for the applicants section.
+"""
+
+from time import time
+import grok
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.applicants.interfaces import IApplicantsUtils
+from waeup.kofa.applicants.workflow import (INITIALIZED,
+    STARTED, PAID, ADMITTED, NOT_ADMITTED, SUBMITTED, CREATED, PROCESSED)
+
+class ApplicantsUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(IApplicantsUtils)
+
+    #: A dictionary containing application type names, titles and
+    #: access code prefixes (meanwhile deprecated).
+    APP_TYPES_DICT = {
+      'app': ['General Studies', 'APP'],
+      'special': ['Special Application', 'SPE'],
+      }
+
+    #: A dictionary which maps widget names to headlines.
+    #: The headline is rendered in forms and on pdf slips above the
+    #: respective display or input widget.
+    SEPARATORS_DICT = {
+      'form.applicant_id': _(u'Base Data'),
+      'form.course1': _(u'Desired Study Courses'),
+      'form.notice': _(u'Application Process Data'),
+      }
+
+    #: A tuple of tuple of file names to be uploaded by applicants and copied 
+    #: over to the students section.
+    ADDITIONAL_FILES = (('Test File','testfile.pdf'),)
+
+    def setPaymentDetails(self, container, payment, applicant):
+        """Set the payment data of an applicant.
+        In contrast to its `StudentsUtils` counterpart, the payment ticket
+        must exist and is an argument of this method.
+        """
+        timestamp = ("%d" % int(time()*10000))[1:]
+        session = str(container.year)
+        try:
+            session_config = grok.getSite()['configuration'][session]
+        except KeyError:
+            return _(u'Session configuration object is not available.')
+        payment.p_id = "p%s" % timestamp
+        payment.p_item = container.title
+        payment.p_session = container.year
+        payment.amount_auth = 0.0
+        if applicant.special:
+            if applicant.special_application:
+                fee_name = applicant.special_application + '_fee'
+                payment.amount_auth = getattr(session_config, fee_name, None)
+                if payment.amount_auth in (0.0, None):
+                    return _('Amount could not be determined.')
+                payment.p_category = applicant.special_application
+            return
+        payment.p_category = 'application'
+        container_fee = container.application_fee
+        if not container_fee:
+            return _('Amount could not be determined.')
+        payment.amount_auth = container_fee
+        return
+
+    def getApplicantsStatistics(self, container):
+        """Count applicants in applicants containers.
+        """
+        state_stats = {INITIALIZED:0, STARTED:0, PAID:0, SUBMITTED:0,
+            ADMITTED:0, NOT_ADMITTED:0, CREATED:0, PROCESSED:0}
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        code = container.code
+        for state in state_stats:
+            if state == 'initialized':
+                results = cat.searchResults(
+                                state=(state, state),
+                                container_code=(code + '+', code + '+'))
+                state_stats[state] = len(results)
+            else:
+                results = cat.searchResults(
+                    state=(state, state),
+                    container_code=(code + '+', code + '-'))
+                state_stats[state] = len(results)
+        return state_stats, None
+
+    def sortCertificates(self, context, resultset):
+        """Sort already filtered certificates in `AppCatCertificateSource`.
+        Display also current course even if certificate in the academic
+        section has been removed.
+        """
+        resultlist = sorted(resultset, key=lambda value: value.code)
+        curr_course = context.course1
+        if curr_course is not None and curr_course not in resultlist:
+            resultlist = [curr_course,] + resultlist
+        return resultlist
+
+    def getCertTitle(self, context, value):
+        """Compose the titles in `AppCatCertificateSource`.
+        """
+        return "%s - %s" % (value.code, value.title)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/viewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/viewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/viewlets.py	(revision 16045)
@@ -0,0 +1,300 @@
+## $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
+##
+import grok
+from waeup.kofa.interfaces import IKofaObject
+from waeup.kofa.students.viewlets import PrimaryStudentNavTab
+from waeup.kofa.browser.viewlets import (
+    ManageActionButton, PrimaryNavTab, AddActionButton)
+from waeup.kofa.applicants.interfaces import (
+    IApplicant, IApplicantsRoot, IApplicantsContainer,
+    IApplicantOnlinePayment
+    )
+from waeup.kofa.applicants.browser import (
+    ApplicantsRootPage, ApplicantsContainerPage, ApplicantManageFormPage,
+    ApplicantDisplayFormPage, OnlinePaymentDisplayFormPage,
+    ApplicantsContainerManageFormPage, ApplicantsStatisticsPage
+    )
+
+from waeup.kofa.interfaces import MessageFactory as _
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+class ApplicantsAuthTab(PrimaryNavTab):
+    """Applicants tab in primary navigation.
+    """
+    grok.context(IKofaObject)
+    grok.order(3)
+    grok.require('waeup.viewApplicantsTab')
+    pnav = 3
+    tab_title = _(u'Applicants')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('applicants')
+
+class ApplicantsAnonTab(ApplicantsAuthTab):
+    """Applicants tab in primary navigation.
+
+    Display tab only for anonymous. Authenticated users can call the
+    form from the user navigation bar.
+    """
+    grok.require('waeup.Anonymous')
+    tab_title = _(u'Application')
+
+    # Also zope.manager has role Anonymous.
+    # To avoid displaying this tab, we have to check the principal id too.
+    @property
+    def link_target(self):
+        if self.request.principal.id == 'zope.anybody':
+            return self.view.application_url('applicants')
+        return
+
+class MyApplicationDataTab(PrimaryStudentNavTab):
+    """MyData-tab in primary navigation.
+    """
+    grok.order(3)
+    grok.require('waeup.viewMyApplicationDataTab')
+    pnav = 3
+    tab_title = _(u'My Data')
+
+    @property
+    def link_target(self):
+        try:
+            container, application_number = self.request.principal.id.split('_')
+        except ValueError:
+            return
+        rel_link = '/applicants/%s/%s' % (container, application_number)
+        return self.view.application_url() + rel_link
+
+class ApplicantsRootSearchActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.viewApplication')
+    text = _('Find applicants')
+    icon = 'actionicon_search.png'
+    target = '@@search'
+
+class ApplicantsRootManageActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.manageApplication')
+    text = _('Manage applicants section')
+
+class ApplicantRegisterActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.Anonymous')
+    icon = 'actionicon_login.png'
+    text = _('Register for application')
+    target = 'register'
+
+class ApplicantsContainerAddActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerManageFormPage)
+    grok.require('waeup.manageApplication')
+    text = _('Add applicant')
+    target = 'addapplicant'
+
+class ApplicantsContainerPrefillActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerManageFormPage)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_bucketfill.png'
+    text = _('Pre-fill container')
+    target = 'prefill'
+
+class ApplicantsContainerPurgeActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerManageFormPage)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_sweep.png'
+    text = _('Purge container')
+    target = 'purge'
+
+class ApplicantsContainerStatisticsActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerManageFormPage)
+    grok.require('waeup.viewApplicationStatistics')
+    icon = 'actionicon_statistics.png'
+    text = _('Container statistics')
+    target = 'statistics'
+
+class ApplicantsContainerStatisticsActionButton2(
+        ApplicantsContainerStatisticsActionButton):
+    grok.order(5)
+    grok.view(ApplicantsContainerPage)
+
+class ApplicantsContainerManageActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.manageApplication')
+    text = _('Manage container')
+    target = 'manage'
+
+class ExportApplicantsActionButton(ManageActionButton):
+    """ 'Export data' button for faculties.
+    """
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_down.png'
+    text = _('Export application data')
+    target = 'exports'
+    grok.order(4)
+
+creation_warning = _(
+    "'The creation process may take a considerably long time "
+    "depending on the number of student records to be created "
+    "(appr. 0.5s per student). "
+    "All users will be logged out and the portal will be in "
+    "maintenance mode during record creation. You will be automatically "
+    "logged in when the creation process is finished. "
+    "Do you really want to start now?'")
+
+class ApplicantsRootCreateStudentsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.createStudents')
+    icon = 'actionicon_entrance.png'
+    text = _('Create students')
+    target ='createallstudents'
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % creation_warning
+
+class ApplicantsContainerCreateStudentsActionButton(ManageActionButton):
+    grok.order(5)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.createStudents')
+    icon = 'actionicon_entrance.png'
+    text = _('Create students')
+    target ='createallstudents'
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % creation_warning
+
+class ApplicantViewActionButton(ManageActionButton):
+    grok.context(IApplicant)
+    grok.view(ApplicantManageFormPage)
+    grok.require('waeup.viewApplication')
+    icon = 'actionicon_view.png'
+    text = _('View application record')
+    target = 'index'
+
+class ApplicantManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicant)
+    grok.view(ApplicantDisplayFormPage)
+    grok.require('waeup.manageApplication')
+    text = _('Manage application record')
+    target = 'manage'
+
+class ApplicantEditActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicant)
+    grok.view(ApplicantDisplayFormPage)
+    grok.require('waeup.handleApplication')
+    text = _('Edit application record')
+    target ='edit'
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        if self.context.locked or (
+            self.context.__parent__.expired and
+            self.context.__parent__.strict_deadline):
+            return
+        return self.view.url(self.view.context, self.target)
+
+class PDFActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IApplicant)
+    grok.require('waeup.viewApplication')
+    grok.name('pdfactionbutton')
+    icon = 'actionicon_pdf.png'
+    text = _('Download application slip')
+    target = 'application_slip.pdf'
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        if self.context.state in ('initialized', 'started', 'paid') \
+            or self.context.special:
+            return
+        return self.view.url(self.view.context, self.target)
+
+class StudentCreateActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IApplicant)
+    grok.require('waeup.createStudents')
+    icon = 'actionicon_entrance.png'
+    text = _('Create student')
+    target ='createstudent'
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        if self.context.state != 'admitted':
+            return
+        return self.view.url(self.view.context, self.target)
+
+class PaymentReceiptActionButton(ManageActionButton):
+    grok.order(9) # This button should always be the last one.
+    grok.context(IApplicantOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.viewApplication')
+    icon = 'actionicon_pdf.png'
+    text = _('Download payment slip')
+    target = 'payment_slip.pdf'
+
+    @property
+    def target_url(self):
+        #if self.context.p_state != 'paid':
+        #    return ''
+        return self.view.url(self.view.context, self.target)
+
+class ApprovePaymentActionButton(ManageActionButton):
+    grok.order(8)
+    grok.context(IApplicantOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.managePortal')
+    icon = 'actionicon_accept.png'
+    text = _('Approve payment')
+    target = 'approve'
+
+    @property
+    def target_url(self):
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/webservices.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/webservices.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/webservices.py	(revision 16045)
@@ -0,0 +1,71 @@
+## $Id$
+##
+## Copyright (C) 2013 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
+##
+import grok
+from hurry.query.query import Query
+from hurry.query import Eq
+from zope.component import getUtility
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from waeup.kofa.interfaces import IUniversity
+
+
+class ApplicantsXMLRPC(grok.XMLRPC):
+    """Applicant related XMLRPC webservices.
+
+    Please note, that XMLRPC does not support real keyword arguments
+    but positional arguments only.
+    """
+    grok.context(IUniversity)
+
+    @grok.require('waeup.xmlrpc')
+    def check_applicant_credentials(self, username, password):
+        """Returns applicant data if username and password are valid,
+        None else.
+
+        We only query the applicants authenticator plugin in order not
+        to mix up with other credentials (admins, staff, etc.).
+
+        All additional checks performed by usual applicant
+        authentication apply.
+
+        This method can be used by CAS to authentify applicants for
+        external systems like moodle.
+        """
+        auth = getUtility(IAuthenticatorPlugin, name='applicants')
+        creds = dict(login=username, password=password)
+        principal = auth.authenticateCredentials(creds)
+        if principal is None:
+            return None
+        return dict(email=principal.email, id=principal.id,
+                    type=principal.user_type,
+                    description=principal.description)
+
+    @grok.require('waeup.xmlrpc')
+    def get_applicant_moodle_data(self, identifier=None):
+        """Returns applicant data to update user data and enroll user
+        in Moodle courses.
+
+        """
+        results = Query().searchResults(
+            Eq(('applicants_catalog', 'applicant_id'), identifier))
+        if not results:
+            return None
+        applicant = list(results)[0]
+        return dict(email=applicant.email,
+                    firstname=applicant.firstname,
+                    lastname=applicant.lastname,
+                    )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/applicants/workflow.py	(revision 16045)
@@ -0,0 +1,208 @@
+## $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
+##
+"""Workflow for applicants.
+"""
+import grok
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.kofa.applicants.interfaces import IApplicantBaseData
+from waeup.kofa.interfaces import IObjectHistory, IKofaWorkflowInfo, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
+
+INITIALIZED = 'initialized'
+STARTED = 'started'
+PAID = 'paid'
+SUBMITTED = 'submitted'
+ADMITTED = 'admitted'
+NOT_ADMITTED = 'not admitted'
+CREATED = 'created'
+PROCESSED = 'processed'
+
+
+IMPORTABLE_STATES = (INITIALIZED, STARTED, PAID,
+                     SUBMITTED, ADMITTED, NOT_ADMITTED, PROCESSED)
+
+application_states_dict = {
+    INITIALIZED: _('initialized'),
+    STARTED: _('started'),
+    PAID: _('paid'),
+    SUBMITTED: _('submitted'),
+    ADMITTED: _('admitted'),
+    NOT_ADMITTED: _('not admitted'),
+    CREATED: _('created'),
+    PROCESSED: _('processed'),
+    }
+
+APPLICATION_TRANSITIONS = (
+    Transition(
+        transition_id = 'init',
+        title = _('Initialize application'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Application initialized'),
+        destination = INITIALIZED),
+
+    Transition(
+        transition_id = 'start',
+        title = _('Start application'),
+        msg = _('Application started'),
+        source = INITIALIZED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'pay',
+        title = _('Pay application fee'),
+        msg = _('Payment made'),
+        source = STARTED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'approve',
+        title = _('Approve payment'),
+        msg = _('Payment approved'),
+        source = STARTED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'submit',
+        title = _('Submit application'),
+        msg = _('Application submitted'),
+        source = PAID,
+        destination = SUBMITTED),
+
+    Transition(
+        transition_id = 'admit',
+        title = _('Admit applicant'),
+        msg = _('Applicant admitted'),
+        source = SUBMITTED,
+        destination = ADMITTED),
+
+    Transition(
+        transition_id = 'refuse1',
+        title = _('Refuse application'),
+        msg = _('Application refused'),
+        source = SUBMITTED,
+        destination = NOT_ADMITTED),
+
+    Transition(
+        transition_id = 'process',
+        title = _('Process application'),
+        msg = _('Application processed'),
+        source = SUBMITTED,
+        destination = PROCESSED),
+
+    Transition(
+        transition_id = 'refuse2',
+        title = _('Refuse application'),
+        msg = _('Application refused'),
+        source = ADMITTED,
+        destination = NOT_ADMITTED),
+
+    Transition(
+        transition_id = 'create',
+        title = _('Create student'),
+        msg = _('Student record created'),
+        source = ADMITTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = PAID,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset application to paid'),
+        msg = _('Application reset to paid'),
+        source = SUBMITTED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = SUBMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset5',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = NOT_ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset6',
+        title = _('Reset application to submitted'),
+        msg = _('Application reset'),
+        source = PROCESSED,
+        destination = SUBMITTED),
+
+    )
+
+application_workflow = KofaWorkflow(APPLICATION_TRANSITIONS)
+
+class ApplicationWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Applicant objects to workflow states.
+    """
+    grok.context(IApplicantBaseData)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.application.state'
+    state_id = 'wf.application.id'
+
+class ApplicationWorkflowInfo(KofaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Applicant objects to workflow info objects.
+    """
+    grok.context(IApplicantBaseData)
+    grok.provides(IKofaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = application_workflow
+
+@grok.subscribe(IApplicantBaseData, IWorkflowTransitionEvent)
+def handle_applicant_transition_event(obj, event):
+    """Append message to applicant history and lock form
+    when transition happened.
+    """
+    msg = event.transition.user_data['msg']
+    if event.transition.transition_id == 'create':
+        msg += ' (%s)' % obj.student_id
+        obj.locked = True
+    if event.transition.transition_id == 'submit':
+        obj.locked = True
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    # In some tests we don't have a an applicants root or a user
+    try:
+        applicants_root = grok.getSite()['applicants']
+        applicants_root.logger.info('%s - %s' % (obj.applicant_id,msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.pt	(revision 16045)
@@ -0,0 +1,53 @@
+<html>
+  <head>
+    <title>Async testing</title>
+    <script type="text/javascript">
+$(document).ready(function() {
+  var myLoadingDiv = $("#progressbar");
+  var mySpinner = $("#loadspinner");
+  myLoadingDiv.progressbar({disabled:true});
+  function update() {
+  $.getJSON("status?",  {jobid: "4",}, function(data) {
+    if (data == null) {
+      myLoadingDiv.progressbar("disable");
+      myLoadingDiv.progressbar("value", 0);
+      mySpinner.html('No Data');
+      $("#msg").html("Syncing...");
+      clearInterval(intervalID);
+      //alert('No Data'):
+      }
+    else {
+      myLoadingDiv.progressbar("enable");
+      myLoadingDiv.progressbar("value", data.percent);
+      $("#msg").html(data.msg + ' (' + data.status + ') ');
+      //alert(data.msg);
+      if (data.percent == 100) {
+        mySpinner.html('Job finished.')
+        clearInterval(intervalID);
+        }
+      }
+  });}
+  intervalID = setInterval(update, 500);
+  //update();
+});
+    </script>
+  </head>
+  <body>
+    <h1>Async stuff</h1>
+    <div>Dispatcher: <span tal:content="view/dispatcher"></span></div>
+    <div>Queue: <span tal:content="view/queue"></span></div>
+    <div>Jobs: <span tal:content="structure view/job_status"></span></div>
+    <form method="POST">
+      <input type="submit" name="reload" value="reload" />
+      <input type="submit" name="start" value="start up" />
+    </form>
+    <div id="progressbar"
+	 class="ui-progressbar ui-widget ui-widget-content ui-corner-all">
+    </div>
+    <div id="msg">Hi there</div>
+    <div id="loadspinner">
+      <img src="" alt="Please wait..."
+	   tal:attributes="src view/spinner_url" />
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async.py	(revision 16045)
@@ -0,0 +1,281 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Components for asynchronous job (task) handling, mainly based on
+:mod:`zc.async`.
+"""
+import grok
+import time
+import transaction
+import zc.async.dispatcher
+import zc.async.utils
+import zope.component
+from zc.async.job import Job as AsyncJob
+from zc.async.queue import getDefaultQueue
+from zc.async.interfaces import IJob, ObjectAdded
+from ZODB.utils import u64
+from zope.component import getUtility, adapter
+from zope.component.hooks import getSite
+from zope.event import notify
+from waeup.kofa.interfaces import (
+    IJobManager, IProgressable, IJobContainer, WAEUP_KEY, IKofaUtils,
+    )
+
+
+def compute(num, duration=60):
+    """A convenience function to test asynchronous jobs.
+
+    `duration` gives the seconds, this job should (artificially) need
+    for completing.
+    """
+    start = time.time()
+    end = start + duration
+    print "MyJob starts computation at ", start
+    myjob = zc.async.local.getJob()
+    print "MyJob's job: ", zc.async.local.getJob()
+    print "MyJob's queue: ", zc.async.local.getQueue(), list(
+        zc.async.local.getQueue())
+    while True:
+        if time.time() > end:
+            break
+        time.sleep(1)
+        percent = (time.time() - start) * 100.0 / duration
+        if percent > 100.0:
+            percent = 100.0
+        print "MyJob percent: ", percent
+        zc.async.local.setLiveAnnotation('percent', percent)
+        print "MyJob does something at %s of %s" % (
+            time.time() - start, duration)
+        print "MyJob's annotations: %r" % dict(myjob.annotations)
+        print "MyJob's percent: %r" % zc.async.local.getLiveAnnotation(
+            'percent')
+    zc.async.local.setLiveAnnotation('percent', 100.0)
+    return num * 2
+
+#
+# Content components
+#
+class ProgressableJob(AsyncJob):
+    """A job that can indicate its progress via a `percent` attribute.
+    """
+    grok.implements(IJob, IProgressable)
+    grok.provides(IJob)
+    percent = None
+
+
+def get_job_id(persisted_job):
+    """Get the object id of an already persisted job.
+
+    The `persisted_job` must provide a `_p_oid` attribute.
+    """
+    job_id = u64(persisted_job._p_oid)
+    return "%s" % job_id
+
+class JobContainer(grok.Container):
+    """A container for :class:`IKofa` jobs.
+    """
+    grok.implements(IJobContainer)
+
+
+class JobManager(grok.GlobalUtility):
+    """A manager for asynchronous running jobs (tasks).
+
+    Registered as a global utility for the
+    `waeup.kofa.interfaces.IJobManager` interface.
+
+    This is the central location for managing asynchronous running
+    jobs/tasks.
+
+    It works roughly like this: for usual tasks it looks up some
+    JobContainer installed in a ZODB database root (the installation
+    can happen during startup; see the respective installer classes
+    and functions in this module) and then interacts with this
+    JobContainer.
+
+    The optional `site` parameter for most methods in here serves for
+    finding the databases' roots. It is sufficient to pass any
+    persisted object (or more precisely: some object with a valid
+    ``_p_jar__`` attribte). As long as some site was already set (for
+    instance during regular requests), the site is looked up
+    automatically and you don't have to pass the `site` parameter
+    then. So, in most cases you won't have to give a `site` parameter.
+    """
+    grok.implements(IJobManager)
+    grok.provides(IJobManager)
+
+    def _get_site(self, site):
+        # in fact we get some persisted object if available.
+        # As sites are normally persisted and easy to lookup, we use them
+        # to get _some_ persisted object.
+        if not hasattr(site, '_p_jar'):
+            site = getSite()
+            if site is None:
+                raise LookupError('cannot find a valid site')
+        return site
+
+    def _get_jobs_container(self, site):
+        # in fact we need _some_ persisted object, not necessarily a site
+        return site._p_jar.root()[WAEUP_KEY]
+
+    def put(self, job, site=None):
+        """Start the `job` and store it in local `site`.
+
+        The `job` must be an `IJob` instance.
+
+        It will be put into the default queue and then stored in local
+        site. The status of the job can be seen immediately in
+        `job.status`.
+
+        Please specify special treatments like `begin_after` or
+        `begin_by` by setting the respectives attributes of the job
+        itself.
+        """
+        site = self._get_site(site)
+        if not getUtility(IKofaUtils).expensive_actions_allowed():
+            return None
+        container = self._get_jobs_container(site)
+        transaction.begin()
+        queue = getDefaultQueue(site)
+        new_job = queue.put(job)
+        job_id = get_job_id(new_job)
+        container[job_id] = new_job
+        transaction.commit()
+        return job_id
+
+    def get(self, job_id, site=None):
+        """Get the job with `job_id` from local `site`.
+
+        If `job_id` cannot be found, ``None`` is returned. This
+        suitable e.g. when used with a traverser.
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        return container.get(job_id, None)
+
+    def jobs(self, site=None):
+        """Get all stored jobs as an iterable.
+
+        Result provides tuples (JOB_ID, JOB_OBJECT).
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        for job_id, job in container.items():
+            yield (job_id, job)
+
+    def remove(self, job_id, site=None):
+        """Remove job with `job_id` from local job container.
+
+        If no such job can be found this is silently ignored.
+
+        Please note: removing a job from the job container does not
+        mean to stop its execution (if it wasn't started yet or is
+        currently running).
+        """
+        site = self._get_site(site)
+        container = self._get_jobs_container(site)
+        if job_id in container.keys():
+            del container[job_id]
+        return
+
+    def start_test_job(self, duration=60, site=None):
+        """Start a test job.
+
+        A method for testing the general asynchronous functionality of
+        waeup.kofa. The test job started here executes the local
+        :func:`compute` function with ``23`` as argument.
+        """
+        job = AsyncJob(compute, 23, duration)
+        job_id = self.put(job, site=site)
+        return job_id
+
+class JobManagerTraverser(grok.Traverser):
+    """A traverser for the global ``IJobManager``.
+
+    Looks up jobs by job_id and returns the respective job if it
+    can be found.
+    """
+    grok.context(IJobManager)
+
+    def traverse(self, name):
+        # ``None`` if name cannot be found.
+        return self.context.get(name)
+
+##
+## configuration (permissions, subscribers, etc.) ...
+##
+
+class ViewJobs(grok.Permission):
+    grok.name('waeup.viewJobs')
+
+class ManageJobs(grok.Permission):
+    grok.name('waeup.manageJobs')
+
+class JobContainerInstaller(object):
+    """Install a JobContainer in root of given DB.
+
+    Instances of this installer can be called when a Zope instance
+    comes up (i.e. when an IDatabaseOpenedEvent was triggered).
+
+    It looks for some database named as in `db_name` and installs a
+    job container in the root of this database (``None`` by default)
+    if it does not exist already.
+    """
+    def __init__(self, db_name=None):
+        # This IDatabaseOpenedEvent will be from zope.app.appsetup if that
+        # package is around
+        adapter(zc.async.interfaces.IDatabaseOpenedEvent)(self)
+        self.db_name = db_name
+        return
+
+    def __call__(self, ev):
+        db = ev.database
+        tm = transaction.TransactionManager()
+        conn = db.open(transaction_manager=tm)
+        tm.begin()
+        try:
+            try:
+                root = conn.root()
+                if WAEUP_KEY in root:
+                    return
+                if self.db_name is not None:
+                    other = conn.get_connection(self.db_name)
+                    container = other.root()[WAEUP_KEY] = JobContainer()
+                    other.add(container)
+                else:
+                    container = JobContainer()
+                root[WAEUP_KEY] = container
+                notify(ObjectAdded(container, root, WAEUP_KEY))
+                tm.commit()
+                zc.async.utils.log.info('job container added')
+            except:
+                tm.abort()
+                raise
+        finally:
+            conn.close()
+        return
+
+#: Can be used as event subscriber from ZCML; installs a job container
+#: in default database (named ``''``) root.
+job_container_installer = JobContainerInstaller()
+
+#: An installer instance that installs a job container in a ZODB
+#: called ``async`` - this name is used in several components of the
+#: `zc.async` package we don't want to reimplement here.
+#:
+#: To use this installer as an event subscriber by ZCML, make sure the
+#: instance provides also a ZODB called `async`.
+multidb_job_container_installer = JobContainerInstaller(db_name='async')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_multidb.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_multidb.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_multidb.zcml	(revision 16045)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".async.multidb_job_container_installer" />
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_single.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_single.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/async_single.zcml	(revision 16045)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".async.job_container_installer" />
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/authentication.py	(revision 16045)
@@ -0,0 +1,700 @@
+## $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
+##
+"""Authentication for Kofa.
+"""
+import grok
+import time
+import re
+from zope.i18n import translate
+from zope.event import notify
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.interfaces import IFactory
+from zope.interface import Interface, implementedBy
+from zope.schema import getFields
+from zope.securitypolicy.interfaces import (
+    IPrincipalRoleMap, IPrincipalRoleManager)
+from zope.pluggableauth.factories import Principal
+from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
+from zope.pluggableauth.plugins.httpplugins import (
+    HTTPBasicAuthCredentialsPlugin)
+from zope.pluggableauth.interfaces import (
+    ICredentialsPlugin, IAuthenticatorPlugin, IAuthenticatedPrincipalFactory,
+    AuthenticatedPrincipalCreated)
+from zope.publisher.interfaces import IRequest
+from zope.password.interfaces import IPasswordManager
+from zope.securitypolicy.principalrole import principalRoleManager
+from waeup.kofa.interfaces import (
+    ILocalRoleSetEvent, IUserAccount, IAuthPluginUtility, IPasswordValidator,
+    IKofaPrincipal, IKofaPrincipalInfo, IKofaPluggable, IBatchProcessor,
+    IGNORE_MARKER, IFailedLoginInfo)
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.permissions import get_all_roles
+from waeup.kofa.interfaces import MessageFactory as _
+
+
+def setup_authentication(pau):
+    """Set up plugguble authentication utility.
+
+    Sets up an IAuthenticatorPlugin and
+    ICredentialsPlugin (for the authentication mechanism)
+
+    Then looks for any external utilities that want to modify the PAU.
+    """
+    pau.credentialsPlugins = (
+        'No Challenge if Authenticated',
+        'xmlrpc-credentials',
+        'credentials')
+    pau.authenticatorPlugins = ('users',)
+
+    # Give any third-party code and subpackages a chance to modify the PAU
+    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
+    for name, util in auth_plugin_utilities:
+        util.register(pau)
+
+
+def get_principal_role_manager():
+    """Get a role manager for principals.
+
+    If we are currently 'in a site', return the role manager for the
+    portal or the global rolemanager else.
+    """
+    portal = grok.getSite()
+    if portal is not None:
+        return IPrincipalRoleManager(portal)
+    return principalRoleManager
+
+
+class KofaSessionCredentialsPlugin(
+        grok.GlobalUtility, SessionCredentialsPlugin):
+    """Session plugin that picks usernames/passwords from fields in webforms.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+
+class KofaXMLRPCCredentialsPlugin(
+        grok.GlobalUtility, HTTPBasicAuthCredentialsPlugin):
+    """Plugin that picks useranams/passwords from basic-auth headers.
+
+    As XMLRPC requests send/post their authentication credentials in HTTP
+    basic-auth headers, we need a plugin that can handle this.
+
+    This plugin, however, does no challenging. If a user does not provide
+    basic-auth infos, we will not ask for some. This is correct as we plan to
+    communicate with machines.
+
+    This plugin is planned to be used in "PluggableAuthenitications" registered
+    with `University` instances.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('xmlrpc-credentials')
+
+    def challenge(self, request):
+        """XMLRPC is for machines. No need to challenge.
+        """
+        return False
+
+    def logout(self, request):
+        """Basic auth does not provide any logout possibility.
+        """
+        return False
+
+
+class KofaPrincipalInfo(object):
+    """An implementation of IKofaPrincipalInfo.
+
+    A Kofa principal info is created with id, login, title, description,
+    phone, email, public_name and user_type.
+    """
+    grok.implements(IKofaPrincipalInfo)
+
+    def __init__(self, id, title, description, email, phone, public_name,
+                 user_type):
+        self.id = id
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.user_type = user_type
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+
+    def __eq__(self, obj):
+        default = object()
+        result = []
+        for name in ('id', 'title', 'description', 'email', 'phone',
+                     'public_name', 'user_type', 'credentialsPlugin',
+                     'authenticatorPlugin'):
+            result.append(
+                getattr(self, name) == getattr(obj, name, default))
+        return False not in result
+
+
+class KofaPrincipal(Principal):
+    """A portal principal.
+
+    Kofa principals provide an extra `email`, `phone`, `public_name`
+    and `user_type` attribute extending ordinary principals.
+    """
+
+    grok.implements(IKofaPrincipal)
+
+    def __init__(self, id, title=u'', description=u'', email=u'',
+                 phone=None, public_name=u'', user_type=u'', prefix=None):
+        self.id = id
+        if prefix is not None:
+            self.id = '%s.%s' % (prefix, self.id)
+        self.title = title
+        self.description = description
+        self.groups = []
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.user_type = user_type
+
+    def __repr__(self):
+        return 'KofaPrincipal(%r)' % self.id
+
+
+class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter):
+    """Creates 'authenticated' Kofa principals.
+
+    Adapts (principal info, request) to a KofaPrincipal instance.
+
+    This adapter is used by the standard PAU to transform
+    KofaPrincipalInfos into KofaPrincipal instances.
+    """
+    grok.adapts(IKofaPrincipalInfo, IRequest)
+    grok.implements(IAuthenticatedPrincipalFactory)
+
+    def __init__(self, info, request):
+        self.info = info
+        self.request = request
+
+    def __call__(self, authentication):
+        principal = KofaPrincipal(
+            self.info.id,
+            self.info.title,
+            self.info.description,
+            self.info.email,
+            self.info.phone,
+            self.info.public_name,
+            self.info.user_type,
+            authentication.prefix,
+            )
+        notify(
+            AuthenticatedPrincipalCreated(
+                authentication, principal, self.info, self.request))
+        return principal
+
+
+class FailedLoginInfo(grok.Model):
+    grok.implements(IFailedLoginInfo)
+
+    def __init__(self, num=0, last=None):
+        self.num = num
+        self.last = last
+        return
+
+    def as_tuple(self):
+        return (self.num, self.last)
+
+    def set_values(self, num=0, last=None):
+        self.num, self.last = num, last
+        self._p_changed = True
+        pass
+
+    def increase(self):
+        self.set_values(num=self.num + 1, last=time.time())
+        pass
+
+    def reset(self):
+        self.set_values(num=0, last=None)
+        pass
+
+
+class Account(grok.Model):
+    """Kofa user accounts store infos about a user.
+
+    Beside the usual data and an (encrypted) password, accounts also
+    have a persistent attribute `failed_logins` which is an instance
+    of `waeup.kofa.authentication.FailedLoginInfo`.
+
+    This attribute can be manipulated directly (set new value,
+    increase values, or reset).
+    """
+    grok.implements(IUserAccount)
+
+    def __init__(self, name, password, title=None, description=None,
+                 email=None, phone=None, public_name=None, roles=[]):
+        self.name = name
+        if title is None:
+            title = name
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.public_name = public_name
+        self.suspended = False
+        self.setPassword(password)
+        self.setSiteRolesForPrincipal(roles)
+
+        # We don't want to share this dict with other accounts
+        self._local_roles = dict()
+        self.failed_logins = FailedLoginInfo()
+
+    def setPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        try:
+            blocker = grok.getSite()['configuration'].maintmode_enabled_by
+            if blocker and blocker != self.name:
+                return False
+        except (TypeError, KeyError):  # in unit tests
+            pass
+        if not isinstance(password, basestring):
+            return False
+        if not self.password:
+            # unset/empty passwords do never match
+            return False
+        # Do not accept password if password is insecure.
+        validator = getUtility(IPasswordValidator)
+        if validator.validate_secure_password(password, password):
+            return False
+        if self.suspended:
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        return passwordmanager.checkPassword(self.password, password)
+
+    def getSiteRolesForPrincipal(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+
+    def setSiteRolesForPrincipal(self, roles):
+        prm = get_principal_role_manager()
+        old_roles = self.getSiteRolesForPrincipal()
+        if sorted(old_roles) == sorted(roles):
+            return
+        for role in old_roles:
+            # Remove old roles, not to be set now...
+            if role.startswith('waeup.') and role not in roles:
+                prm.unsetRoleForPrincipal(role, self.name)
+        for role in roles:
+            # Convert role to ASCII string to be in line with the
+            # event handler
+            prm.assignRoleToPrincipal(str(role), self.name)
+        return
+
+    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
+
+    def getLocalRoles(self):
+        return self._local_roles
+
+    def notifyLocalRoleChanged(self, obj, role_id, granted=True):
+        objects = self._local_roles.get(role_id, [])
+        if granted and obj not in objects:
+            objects.append(obj)
+        if not granted and obj in objects:
+            objects.remove(obj)
+        self._local_roles[role_id] = objects
+        if len(objects) == 0:
+            del self._local_roles[role_id]
+        self._p_changed = True
+        return
+
+
+class UserAuthenticatorPlugin(grok.GlobalUtility):
+    grok.implements(IAuthenticatorPlugin)
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('users')
+
+    def authenticateCredentials(self, credentials):
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+        account = self.getAccount(credentials['login'])
+        if account is None:
+            return None
+        # The following shows how 'time penalties' could be enforced
+        # on failed logins. First three failed logins are 'for
+        # free'. After that the user has to wait for 1, 2, 4, 8, 16,
+        # 32, ... seconds before a login can succeed.
+        # There are, however, some problems to discuss, before we
+        # really use this in all authenticators.
+
+        #num, last = account.failed_logins.as_tuple()
+        #if (num > 2) and (time.time() < (last + 2**(num-3))):
+        #    # tried login while account still blocked due to previous
+        #    # login errors.
+        #    return None
+        if not account.checkPassword(credentials['password']):
+            #account.failed_logins.increase()
+            return None
+        return KofaPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            public_name=account.public_name,
+            user_type=u'user')
+
+    def principalInfo(self, id):
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return KofaPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            public_name=account.public_name,
+            user_type=u'user')
+
+    def getAccount(self, login):
+        # ... look up the account object and return it ...
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        return userscontainer.get(login, None)
+
+    def addAccount(self, account):
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        # XXX: complain if name already exists...
+        userscontainer.addAccount(account)
+
+    def addUser(self, name, password, title=None, description=None):
+        userscontainer = self.getUsersContainer()
+        if userscontainer is None:
+            return
+        userscontainer.addUser(name, password, title, description)
+
+    def getUsersContainer(self):
+        site = grok.getSite()
+        return site['users']
+
+
+class PasswordValidator(grok.GlobalUtility):
+
+    grok.implements(IPasswordValidator)
+
+    def validate_password(self, pw, pw_repeat):
+        errors = []
+        if len(pw) < 6:
+            errors.append(translate(_(
+                'Password must have at least 6 characters.')))
+        if pw != pw_repeat:
+            errors.append(translate(_('Passwords do not match.')))
+        return errors
+
+    def validate_secure_password(self, pw, pw_repeat):
+        """
+        ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$
+
+        ^              Start anchor
+        (?=.*[A-Z])    Ensure password has one uppercase letters.
+        (?=.*[0-9])    Ensure password has one digit.
+        (?=.*[a-z])    Ensure password has one lowercase letter.
+        .{8,}          Ensure password is of length 8.
+        $              End anchor
+        """
+        check_pw = re.compile(r"^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$").match
+        errors = []
+        if not check_pw(pw):
+            errors.append(translate(_(
+                'Passwords must be at least 8 characters long, '
+                'must contain at least one uppercase letter, '
+                'one lowercase letter and one digit.')))
+        if pw != pw_repeat:
+            errors.append(translate(_('Passwords do not match.')))
+        return errors
+
+
+class LocalRoleSetEvent(object):
+
+    grok.implements(ILocalRoleSetEvent)
+
+    def __init__(self, object, role_id, principal_id, granted=True):
+        self.object = object
+        self.role_id = role_id
+        self.principal_id = principal_id
+        self.granted = granted
+
+
+@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
+def handle_account_removed(account, event):
+    """When an account is removed, local and global roles might
+    have to be deleted.
+    """
+    local_roles = account.getLocalRoles()
+    principal = account.name
+
+    for role_id, object_list in local_roles.items():
+        for object in object_list:
+            try:
+                role_manager = IPrincipalRoleManager(object)
+            except TypeError:
+                # No Account object, no role manager, no local roles to remove
+                continue
+            role_manager.unsetRoleForPrincipal(role_id, principal)
+    role_manager = IPrincipalRoleManager(grok.getSite())
+    roles = account.getSiteRolesForPrincipal()
+    for role_id in roles:
+        role_manager.unsetRoleForPrincipal(role_id, principal)
+    return
+
+
+@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
+def handle_account_added(account, event):
+    """When an account is added, the local owner role and the global
+    AcademicsOfficer role must be set.
+    """
+    # We set the local Owner role
+    role_manager_account = IPrincipalRoleManager(account)
+    role_manager_account.assignRoleToPrincipal(
+        'waeup.local.Owner', account.name)
+    # We set the global AcademicsOfficer role
+    site = grok.getSite()
+    role_manager_site = IPrincipalRoleManager(site)
+    role_manager_site.assignRoleToPrincipal(
+        'waeup.AcademicsOfficer', account.name)
+    # Finally we have to notify the user account that the local role
+    # of the same object has changed
+    notify(LocalRoleSetEvent(
+        account, 'waeup.local.Owner', account.name, granted=True))
+    return
+
+
+@grok.subscribe(Interface, ILocalRoleSetEvent)
+def handle_local_role_changed(obj, event):
+    site = grok.getSite()
+    if site is None:
+        return
+    users = site.get('users', None)
+    if users is None:
+        return
+    if event.principal_id not in users.keys():
+        return
+    user = users[event.principal_id]
+    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
+    return
+
+
+@grok.subscribe(Interface, grok.IObjectRemovedEvent)
+def handle_local_roles_on_obj_removed(obj, event):
+    try:
+        role_map = IPrincipalRoleMap(obj)
+    except TypeError:
+        # no map, no roles to remove
+        return
+    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+        notify(LocalRoleSetEvent(
+            obj, local_role, user_name, granted=False))
+    return
+
+
+class UserAccountFactory(grok.GlobalUtility):
+    """A factory for user accounts.
+
+    This factory is only needed for imports.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.UserAccount')
+    title = u"Create a user.",
+    description = u"This factory instantiates new user account instances."
+
+    def __call__(self, *args, **kw):
+        return Account(name=None, password='')
+
+    def getInterfaces(self):
+        return implementedBy(Account)
+
+
+class UserProcessor(BatchProcessor):
+    """The User Processor processes user accounts, i.e. `Account` objects in
+    the ``users`` container.
+
+    The `roles` columns must contain Python list
+    expressions like ``['waeup.PortalManager', 'waeup.ImportManager']``.
+
+    The processor does not import local roles. These can be imported
+    by means of batch processors in the academic section.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'userprocessor'
+    grok.name(util_name)
+
+    name = u'User Processor'
+    iface = IUserAccount
+
+    location_fields = ['name', ]
+    factory_name = 'waeup.UserAccount'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'users' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['name'] in site['users'].keys()
+
+    def getParent(self, row, site):
+        return site['users']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['name'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addAccount(obj)
+        return
+
+    def delEntry(self, row, site):
+        user = self.getEntry(row, site)
+        if user is not None:
+            parent = self.getParent(row, site)
+            grok.getSite().logger.info(
+                '%s - %s - User removed' % (self.name, row['name']))
+            del parent[user.name]
+        pass
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        changed = []
+        for key, value in row.items():
+            if key == 'roles':
+                # We cannot simply set the roles attribute here because
+                # we can't assure that the name attribute is set before
+                # the roles attribute is set.
+                continue
+            # Skip fields to be ignored.
+            if value == IGNORE_MARKER:
+                continue
+            if not hasattr(obj, key):
+                continue
+            setattr(obj, key, value)
+            changed.append('%s=%s' % (key, value))
+        roles = row.get('roles', IGNORE_MARKER)
+        if roles not in ('', IGNORE_MARKER):
+            evalvalue = eval(roles)
+            if isinstance(evalvalue, list):
+                setattr(obj, 'roles', evalvalue)
+                changed.append('roles=%s' % roles)
+        # Log actions...
+        items_changed = ', '.join(changed)
+        grok.getSite().logger.info(
+            '%s - %s - %s - updated: %s' % (
+                self.name, filename, row['name'], items_changed))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            UserProcessor, self).checkConversion(row, mode=mode)
+        # We need to check if roles exist.
+        roles = row.get('roles', IGNORE_MARKER)
+        all_roles = [i[0] for i in get_all_roles()]
+        if roles not in ('', IGNORE_MARKER):
+            evalvalue = eval(roles)
+            for role in evalvalue:
+                if role not in all_roles:
+                    errs.append(('roles', 'invalid role'))
+        return errs, inv_errs, conv_dict
+
+
+class UsersPlugin(grok.GlobalUtility):
+    """A plugin that updates users.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('users')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        users = site['users']
+        items = getFields(IUserAccount).items()
+        for user in users.values():
+            # Add new attributes
+            for i in items:
+                if not hasattr(user, i[0]):
+                    setattr(user, i[0], i[1].missing_value)
+                    logger.info(
+                        'UsersPlugin: %s attribute %s added.' % (
+                            user.name, i[0]))
+            if not hasattr(user, 'failed_logins'):
+                # add attribute `failed_logins`...
+                user.failed_logins = FailedLoginInfo()
+                logger.info(
+                    'UsersPlugin: attribute failed_logins added.')
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(user, i)
+                    logger.info(
+                        'UsersPlugin: %s attribute %s deleted.' % (
+                            user.name, i))
+                except AttributeError:
+                    pass
+        return
+
+
+class UpdatePAUPlugin(grok.GlobalUtility):
+    """A plugin that updates a local PAU.
+
+    We insert an 'xmlrpc-credentials' PAU-plugin into a sites PAU if it is not
+    present already. There must be 'credentials' plugin registered already.
+
+    XXX: This Plugin fixes a shortcoming of waeup.kofa 1.5. Sites created or
+         updated afterwards do not need this plugin and it should be removed.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('site-pluggable-auth')
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        pau = site.getSiteManager()['PluggableAuthentication']
+        if 'xmlrpc-credentials' in pau.credentialsPlugins:
+            return
+        plugins = list(pau.credentialsPlugins)
+        plugins.insert(plugins.index('credentials'), 'xmlrpc-credentials')
+        pau.credentialsPlugins = tuple(plugins)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/beaker.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/beaker.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/beaker.py	(revision 16045)
@@ -0,0 +1,60 @@
+"""Configure dolmen.beaker.
+
+One main advantage of using dolmen.beaker is that session data for a
+certain user can be stored almost automatically in a cookie, thus
+reducing the number of needed ZODB operations.
+
+Security Advisory
+-----------------
+
+To prevent users from playing around with their session data, these
+data is encrypted by some keys stored in a dict registered as a global
+utility.
+
+These keys are set each time the Zope instance starts. If they change,
+all existing cookies will become unreadable and therefore the stored
+sessions will be lost. Already logged-in users will have to login
+again and all other session based operations might have to be
+restarted.
+
+Changing the keys might therefore have sideeffects.
+
+On the other hand static keys stored in SVN might become known to
+users and enable them to manipulate their session data.
+
+For better security the keys (or one of the keys) could therefore be
+gathered from 'outside' (a file in filesystem, some environment var,
+or whatever).
+"""
+import grok
+from zope.app.appsetup.interfaces import IDatabaseOpenedWithRootEvent
+from zope.component import getUtility
+
+#: Our configuration for dolmen.beaker sessions.
+#:
+#: See http://gitweb.dolmen-project.org/dolmen.beaker.git?a=blob;f=src/dolmen/beaker/utilities.py
+#:
+#: for default configuration.
+BEAKER_CONFIG = dict(
+    data_dir=None,
+    invalidate_corrupt=True,
+    key='waeup.kofa.session.id',
+    log_file=None,
+    secret="KofaRocks",
+    timeout=600,
+    type="cookie",
+    validate_key="thisMightBeChanged",
+    )
+
+@grok.subscribe(IDatabaseOpenedWithRootEvent)
+def set_beaker_conf(event):
+    # Set beaker conf once when ZODB was opened
+    try:
+        from dolmen.beaker.interfaces import ISessionConfig
+    except ImportError:
+        # we seem to work without dolmen.beaker
+        return
+
+    config = getUtility(ISessionConfig)
+    config.update(BEAKER_CONFIG)
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/__init__.py	(revision 16045)
@@ -0,0 +1,15 @@
+import os
+
+#from waeup.kofa.browser.layout import (
+#    KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
+#    KofaAddFormPage, NullValidator)
+#from waeup.kofa.browser.pages import ContactAdminForm
+
+IMAGE_PATH = os.path.join(
+    os.path.dirname(__file__),
+    'static'
+    )
+
+#: Filesystem paths to default images
+DEFAULT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
+DEFAULT_PASSPORT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder.jpg')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/async.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/async.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/async.py	(revision 16045)
@@ -0,0 +1,148 @@
+import grok
+from zc.async.interfaces import COMPLETED, NEW
+from zope.interface import Interface
+from waeup.kofa.async import IJob, IJobManager
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import IKofaObject, IDataCenter
+from waeup.kofa.interfaces import MessageFactory as _
+
+grok.templatedir('templates')
+grok.context(IKofaObject)
+
+loadingbar_template = '''
+<input id="status_url" type="hidden" name="status_url" value="%s" />
+<div id="loadbar">%%s</div>
+'''
+
+class LoadingBar(grok.ViewletManager):
+    grok.name('loadingbar')
+
+class LoadingBarViewlet(grok.Viewlet):
+    grok.context(Interface)
+    grok.viewletmanager(LoadingBar)
+
+    _running_exports = None
+
+    def getRunningExports(self):
+        """Returns running exports as list of tuples.
+
+        Only exports triggered by the current user (identified by
+        principal.id) are returned.
+
+        Each tuple has the form (<STATUS>, <STATUS_TITLE>, <EXPORTER_NAME>).
+
+        ``STATUS``:
+           the status as machine readable string (something like
+           ``'completed'``)
+
+        ``STATUS_TITLE``:
+           status of export as translated string.
+
+        ``EXPORTER_NAME``:
+           string representing the exporter title used when triggering
+           the export job.
+        """
+        result = self.context.get_export_jobs_status(self.user_id)
+        return result
+
+    def update(self):
+        self.user_id = self.request.principal.id
+        self.exports = self.getRunningExports()
+        self.uncompleted = [x for x in self.exports if x[0] != 'completed']
+
+    def render(self):
+        status_url = self.view.url('status')
+        template = loadingbar_template % (status_url,)
+        content = ''
+        return template % content
+
+class DataCenterJSON(grok.JSON):
+    grok.context(IDataCenter)
+
+    def status(self):
+        """Get status of currently handled export jobs for users.
+
+        The result is a set of valus useful for JavaScript handlers to
+        update some page handling data center exports.
+
+        Returns a mapping::
+
+          {html=<string>, interval=<int>, reload=<bool>}
+
+        where `html` is some HTML code that might be shown in a
+        loading bar section.
+
+        `interval` is the time in microseconds after which the loading
+        bar should be reloaded (by a new JSON call). If the value is
+        zero, then the loading bar should not be renewed and further
+        JSON calls be canceled.
+
+        `reload` is a hint to tell, whether the complete page should
+        be reloaded or not. Reloading is neccessary, if the export
+        file creation finished and download buttons or similar should
+        appear.
+        """
+        user_id = self.request.principal.id
+        jobs = self.context.get_export_jobs_status(user_id)
+        uncompleted = [x for x in jobs if x[0] != 'completed']
+        result = dict(
+            html='', interval=0, reload=False)
+        if len(uncompleted):
+            html = '<em>%s</em>' % (_("Please wait..."),)
+            result.update(interval=500, html=html)
+        else:
+            if len(jobs):
+                result.update(reload=True)
+        return result
+
+
+class JobManagerView(KofaPage):
+    """The main view for the job container.
+    """
+    grok.name('index.html')
+    grok.context(IJobManager)
+    grok.template('virtjobscontainerindex')
+    grok.require('waeup.manageJobs')
+
+    def jobs(self):
+        for job_id, job in sorted(self.context.jobs(),
+                                  cmp=lambda x, y: cmp(int(x[0]), int(y[0]))):
+            link = self.url(self.context, job_id)
+            removable = job.status in (COMPLETED, NEW)
+            yield dict(
+                job_id = job_id,
+                job = job,
+                link = link,
+                status = job.status,
+                begin_after = job.begin_after,
+                begin_by = job.begin_by,
+                annotations = '%r' % [
+                    (x, y) for x, y in job.annotations.items()],
+                removable = removable,
+                failure = '%r' % getattr(job, 'traceback', None),
+                )
+
+    def update(self, START_NEW=None, REMOVE=None, job_id=None):
+        if REMOVE and job_id:
+            self.context.remove(job_id)
+            self.flash('Removed job "%s"' % job_id)
+        if START_NEW:
+            self.context.start_test_job()                    #pragma NO COVER
+            self.flash('Started new test job "%s"' % job_id) #pragma NO COVER
+        return
+
+class JobView(grok.View):
+    grok.name('index.html')
+    grok.context(IJob)
+    grok.template('jobindex')
+    grok.require('waeup.manageJobs')
+
+    def percent(self):
+        value = self.context.annotations.get('percent', None)
+        if not value:
+            return
+        value = int(value+0.5)
+        return min(value, 100)
+
+    def update(self):
+        pass
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/breadcrumbs.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/breadcrumbs.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/breadcrumbs.py	(revision 16045)
@@ -0,0 +1,279 @@
+## $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
+##
+""" Components to get breadcrumbs for any object.
+"""
+import grok
+from grokcore.view.interfaces import IGrokView
+from zope.component import getAdapter
+from zope.publisher.browser import TestRequest
+
+from waeup.kofa.interfaces import (
+    IConfigurationContainer, ISessionConfiguration, IExportJobContainer)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.interfaces import (
+    IBreadcrumb, IBreadcrumbIgnorable, IBreadcrumbContainer, IKofaObject,
+    IUniversity, IFacultiesContainer, IUsersContainer, IDataCenter, IFaculty,
+    IDepartment, ICourse, ICertificate, ICoursesContainer, ICertificateCourse,
+    ICertificatesContainer
+    )
+from waeup.kofa.reports import IReportsContainer
+
+class Breadcrumb(grok.Adapter):
+    """A most general breadcrumb generator.
+    """
+    grok.provides(IBreadcrumb)
+    grok.context(IKofaObject)
+    grok.name('index')
+
+    _title = None
+    _parent = 0
+    _request = None
+    parent_viewname = 'index'
+    viewname = 'index'
+
+    def __init__(self, context):
+        """Turn a context into a breadcrumb.
+        """
+        self.context = context
+
+    @property
+    def title(self):
+        """Get a title for a context.
+        """
+        if self._title is not None:
+            return self._title
+        if hasattr(self.context, 'title'):
+            return self.context.title
+        if hasattr(self.context, 'name'):
+            return self.context.name
+        return None
+
+    @property
+    def parent(self):
+        """Get the contexts parent object and viewname or None.
+        """
+        if self._parent is None:
+            return None
+        if self._parent is not 0:
+            return (self._parent, self.parent_viewname)
+
+        if self.viewname != 'index':
+            self._parent = self.context
+        else:
+            site = grok.getSite()
+            if self.context is not site:
+                self._parent = getattr(self.context, '__parent__', None)
+
+        if self._parent is not None:
+            return (self._parent, self.parent_viewname)
+        return None
+
+    @property
+    def target(self):
+        return self.viewname
+
+class UniversityBreadcrumb(Breadcrumb):
+    """A breadcrumb for university index pages.
+    """
+    grok.context(IUniversity)
+    title = _(u'Home')
+    parent = None
+
+class PortalSettingsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the manage view of universities.
+
+    Here we need a special `parent()` implementation, because the
+    parent object is not a real parent (the University object has no
+    valid parent in terms of breadcrumbs). Instead it is the
+    ``administration`` view of the same context the ``manage`` page
+    itself is bound to.
+    """
+    grok.context(IUniversity)
+    grok.name('manage')
+    title = _(u'Portal Settings')
+
+    @property
+    def parent(self):
+        """Return the 'administration' view of our context as parent.
+        """
+        return (self.context, 'administration')
+
+class FacultiesContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculty containers.
+    """
+    grok.context(IFacultiesContainer)
+    title = _(u'Academics')
+
+class AdministrationBreadcrumb(Breadcrumb):
+    """A breadcrumb for administration areas of University instances.
+    """
+    grok.context(IUniversity)
+    grok.name('administration')
+    title = _(u'Administration')
+    viewname = 'administration'
+
+class ConfigurationContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for the configuration container.
+    """
+    grok.context(IConfigurationContainer)
+    title = _(u'Portal Configuration')
+    parent_viewname = 'administration'
+
+class SessionConfigurationBreadcrumb(Breadcrumb):
+    """A breadcrumb for the configuration container.
+    """
+    grok.context(ISessionConfiguration)
+    title = u'Portal Session Configuration'
+
+    @property
+    def title(self):
+        session_string = self.context.getSessionString()
+        return 'Session %s' % session_string
+
+class UsersContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for user containers.
+    """
+    grok.context(IUsersContainer)
+    title = _(u'Officers')
+    parent_viewname = 'administration'
+
+class DataCenterBreadcrumb(Breadcrumb):
+    """A breadcrumb for data centers.
+    """
+    grok.context(IDataCenter)
+    title = _(u'Data Center')
+    parent_viewname = 'administration'
+
+class ReportsBreadcrumb(Breadcrumb):
+    """A breadcrumb for reports.
+    """
+    grok.context(IReportsContainer)
+    title = _(u'Reports')
+    parent_viewname = 'administration'
+    target = None
+
+class ExportsBreadcrumb(Breadcrumb):
+    """A breadcrumb for exports.
+    """
+    grok.context(IExportJobContainer)
+    title = _(u'Student Data Exports')
+    target = None
+
+class FacultyBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculties.
+    """
+    grok.context(IFaculty)
+
+    @property
+    def title(self):
+        return self.context.longtitle
+
+class DepartmentBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for departments.
+    """
+    grok.context(IDepartment)
+
+class CourseBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for courses.
+    """
+    grok.context(ICourse)
+
+class CertificateBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for certificates.
+    """
+    grok.context(ICertificate)
+
+class CoursesContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(ICoursesContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificatesContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(ICertificatesContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificateCourseBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(ICertificateCourse)
+    @property
+    def title(self):
+        return self.context.longtitle
+
+def getBreadcrumb(obj, viewname=None):
+    """ Get a breadcrumb for an object and a viewname.
+
+    If there is no breadcrumb defined for such a combination, a
+    breadcrumb for the ``index`` view will be looked up.
+    """
+    try:
+        return getAdapter(obj, IBreadcrumb, name=viewname)
+    except:
+        pass
+    return getAdapter(obj, IBreadcrumb, name='index')
+
+def getBreadcrumbList(obj, viewname):
+    """Get an ordered list of breadcrumbs for an object and a viewname.
+
+    Ignorables are excluded from the result.
+    """
+    current = getBreadcrumb(obj, viewname)
+    result = [current]
+    while current.parent is not None:
+        context, viewname = current.parent
+        current = getBreadcrumb(context, viewname)
+        if IBreadcrumbIgnorable.providedBy(current):
+            # Ignore empty breadcrumbs...
+            continue
+        result.append(current)
+    result.reverse()
+    return result
+
+def getBreadcrumbListForView(view):
+    """Get an ordered list of breadcrumbs a certain view.
+
+    Ignorables are excluded from the result.
+    """
+    context = getattr(view, 'context')
+    viewname = getattr(view, '__name__')
+    return getBreadcrumbList(context, viewname)
+
+class BreadcrumbContainer(grok.Adapter):
+    """An adapter to adapt grok views to list of breadcrumbs.
+    """
+    grok.context(IGrokView)
+    grok.provides(IBreadcrumbContainer)
+
+    _breadcrumbs = None
+
+    def __init__(self, context):
+        self.context = context
+        self._breadcrumbs = getBreadcrumbListForView(self.context)
+
+    def __iter__(self):
+        """Allow iteration.
+        """
+        return self._breadcrumbs.__iter__()
+
+    def getList(self):
+        """Get the (ordered) list of breadcrumbs liked to the context view.
+        """
+        return self._breadcrumbs
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/captcha.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/captcha.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/captcha.py	(revision 16045)
@@ -0,0 +1,333 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## 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
+##
+"""Components to add captcha functionality in views/pages.
+
+This is currently a playground, stuff still to be worked out more properly.
+"""
+import grok
+import json
+import urllib
+import urllib2
+from random import SystemRandom as random
+from zope import schema
+from zope.component import getUtilitiesFor, getUtility, queryUtility
+from zope.interface import Interface
+from zope.publisher.interfaces.http import IHTTPRequest
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.browser.interfaces import (
+    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
+    ICaptchaManager)
+from waeup.kofa.interfaces import IUniversity
+
+#
+# Global captcha manager
+#
+class CaptchaManager(grok.GlobalUtility):
+
+    grok.implements(ICaptchaManager)
+
+    def getAvailCaptchas(self):
+        """Get all available captchas registered as utils for ICaptcha.
+
+        The default captcha (as it most probably is a copy of another
+        registered captcha) is left out of the result.
+
+        Result will be a dict with registration names as keys and the
+        specific captcha instances as values.
+        """
+        result = getUtilitiesFor(ICaptcha)
+        return dict([(name,inst) for name,inst in result
+                       if name != u''])
+
+    def getCaptcha(self):
+        """Get captcha chosen to be used.
+
+        Sites can activate a specific captcha by setting
+        ``site['configuration'].captcha``. The attribute should be a
+        string under which the specific captcha is registered.
+
+        If this attribute is not set or we are
+        not 'in a site', the default captcha is returned.
+        """
+        site = grok.getSite()
+        name = ''
+        if site is None:
+            return getUtility(ICaptcha)
+        name = getattr(site.get('configuration', {}), 'captcha', u'')
+        return queryUtility(ICaptcha, name=name,
+                            default=getUtility(ICaptcha))
+
+##
+## Trivial default captcha
+##
+class CaptchaResponse(object):
+    grok.implements(ICaptchaResponse)
+    def __init__(self, is_valid, error_code=None):
+        self.is_valid = is_valid
+        self.error_code = error_code
+        return
+
+class CaptchaRequest(object):
+    grok.implements(ICaptchaRequest)
+    def __init__(self, solution=None, challenge=None):
+        self.solution = solution
+        self.challenge = challenge
+        return
+
+class NullCaptcha(object):
+    """A captcha that does not expect any input.
+
+    Meant as a placeholder for sites that do not want captchas at all.
+
+    NullCaptchas do not render any HTML/JavaScript and accept any
+    request when asked for verification.
+
+    They can be used in pages prepared for captchas where the site
+    maintainer decides not to use it at all.
+    """
+    grok.implements(ICaptcha)
+
+    def verify(self, request):
+        return CaptchaResponse(True, None)
+
+    def display(self, error_code=None):
+        return u''
+
+
+# This captcha is registered twice: one time as a 'no captcha' captcha
+# and then also as the default captcha (with empty name)
+grok.global_utility(NullCaptcha, name=u'No captcha')
+grok.global_utility(NullCaptcha, name=u'')
+
+##
+## TestCaptcha
+##
+class StaticCaptcha(object):
+    """The StaticCaptcha always has the same solution: 'the-solution'.
+
+    It is of no use for real world but for tests. In tests we cannot
+    easily solve really strong captchas. But we can use a captcha that
+    works like a real one but is easy to solve even for machines.
+
+    The HTML form piece generated is even prefilled with the correct
+    solution. So in tests it is not necessary to always 'type' the
+    solution string in the correct field.
+
+    You can, however, fill in a wrong solution and it will be detected
+    as such.
+    """
+    grok.implements(ICaptcha)
+
+    #: name of solution field in HTTP request
+    sol_field = 'solution'
+    #: name of challenge field in HTTP request
+    chal_field = 'challenge'
+
+    def verify(self, request):
+        """Verify that a solution sent equals the challenge.
+        """
+        form = getattr(request, 'form', {})
+        solution=form.get(self.sol_field, None)
+        challenge=form.get(self.chal_field, None)
+        if solution == challenge and solution:
+            return CaptchaResponse(is_valid=True)
+        return CaptchaResponse(is_valid=False)
+
+    def display(self, error_code=None):
+        """Display challenge and input field for solution as HTML.
+        """
+        html = (
+            u'<input type="hidden" name="challenge"'
+            u'       value="the-solution" /><br />'
+            u'Type: %s<br />'
+            u'<input type="text" name="solution" value="the-solution"'
+            u' /><br />')
+        return html
+
+grok.global_utility(StaticCaptcha, name=u'Testing captcha')
+
+
+##
+## ReCaptcha (v2)
+##
+class ReCaptcha(StaticCaptcha):
+    """ReCaptcha - strong captchas with images, sound, etc.
+
+    This is the Kofa implementation to support captchas as provided by
+    http://www.google.com/recaptcha (v2).
+
+    ReCaptcha is widely used and adopted in web applications. See the
+    above web page to learn more about recaptcha.
+
+    Basically, it generates a captcha box in a page loaded by a
+    client. The client can then enter a solution for a challenge
+    picture (or audio file) and send the solution back to our server
+    along with the challenge string generated locally on the client.
+
+    This component then verifies the entered solution deploying the
+    private key set by asking a verification server. The result (valid
+    or invalid solution) is then returned to any calling component.
+
+    As any captcha-component, :class:`ReCaptcha` can be used by any
+    other component that wants to display/verify captchas.
+
+    To incorporate captcha usage in a view, page, or viewlet, the
+    following steps have to be performed:
+
+    * get the currently site-wide selected captcha type by doing::
+
+        mycaptcha = getUtility(ICaptchaManager).getCaptcha()
+
+    * if you want a specific captcha type (like ReCaptcha)::
+
+        mycaptcha = getUtility(ICaptcha, name='ReCaptcha')
+
+    Now, as you have a captcha, you can verify sent data by doing::
+
+        result = mycaptcha.verify(self.request)
+
+    where ``self.request`` should be the sent HTTP request and
+    ``result`` will be an instance of class:``CaptchaResponse``. The
+    response will contain an error code (``result.error_code``) that
+    might be ``None`` but can (and should) be passed to the
+    :meth:``display`` method to display error messages in the captcha
+    box. The error code is most probably not a human readable string
+    but some code you shouldn't rely upon.
+
+    All this could be done in the ``update()`` method of a view, page,
+    or viewlet.
+
+    To render the needed HTML code, you can deploy the
+    :meth:`display`` method of ``mycaptcha``.
+
+    This captcha is available at runtime as a global utility named
+    ``'ReCaptcha'``.
+
+    Shortcomings:
+        - no support for selecting theme
+        - no support for selecting box layout (normal, compact)
+        - no support for non-javascript users
+        - Google will get the data of our clients
+        - Google will track all activity of our clients
+    """
+
+    grok.implements(ICaptcha)
+
+    #: name of response token field in HTTP request
+    token_field = 'g-recaptcha-response'
+
+    # Do not use the following keys in productive environments!  As
+    # they are both made publicly available, they are not secure any
+    # more!  Use them for testing and evaluating only!
+    PUBLIC_KEY = "6Lc0y8oSAAAAAHwdojrqPtcKn7Rww5qGprb0rrSk"
+    PRIVATE_KEY = "6Lc0y8oSAAAAAMHVbMrGWLLjw2pm8v2Uprwm9AbR"
+
+    VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'
+
+    def verify(self, request):
+        """Grab challenge/solution from HTTP request and verify it.
+
+        Verification happens against recaptcha remote API servers. It
+        only happens, when a repsonse token was sent with the
+        request.
+
+        Returns a :class:`CaptchaResponse` indicating that the
+        verification failed or succeeded.
+        """
+        form = getattr(request, 'form', {})
+        token = form.get(self.token_field, None)
+        if not token:
+            # on first-time display, we won't get a token
+            return CaptchaResponse(is_valid=False)
+        params = urllib.urlencode(
+            {
+                'secret': self.PRIVATE_KEY,
+                'response': token,
+                'remoteip': '127.0.0.1',
+                })
+        request = urllib2.Request(
+            url = self.VERIFY_URL,
+            data = params,
+            headers = {
+                "Content-type": "application/x-www-form-urlencoded",
+                "User-agent": "reCAPTCHA Python Kofa",
+                }
+            )
+        conn = urllib2.urlopen(request)
+        ret_vals = json.loads(conn.read())
+        conn.close()
+        if ret_vals.get('success', False) is True:
+            return CaptchaResponse(is_valid=True)
+        return CaptchaResponse(
+            is_valid=False, error_code="%s" % ret_vals['error-codes'])
+
+    def display(self, error_code=None):
+        """Display captcha widget snippet.
+
+        Returns the HTML code to be placed inside an existing ``<form>``
+        of your page. You can add other fields and should add a submit
+        button to send the form.
+
+        The ``error_code`` can be taken from a previously fetched
+        :class:`CaptchaResponse` instance (as returned by
+        :meth:`verify`). If it is not ``None``, it might be
+        displayed inside the generated captcha box (in human readable
+        form).
+        """
+        error_param = ''
+        if error_code:
+            error_param = '&error=%s' % error_code
+
+        html = (
+            u'<script type="text/javascript" '
+            u'src="%(ApiServer)s" async defer>'
+            u'</script>'
+            u'<div class="g-recaptcha" data-sitekey="%(SiteKey)s"></div>' % {
+                'ApiServer': "https://www.google.com/recaptcha/api.js",
+                'SiteKey': self.PUBLIC_KEY,
+                }
+        )
+        return html
+
+
+grok.global_utility(ReCaptcha, name=u'ReCaptcha')
+
+
+class CaptchaTestPage(KofaPage):
+    # A test page to see a captcha in action
+    grok.name('captcha')
+    grok.context(IUniversity)
+    grok.require('waeup.Public')
+    title = 'Captcha Test'
+    label = title
+
+    def update(self, recaptcha_challenge_field=None,
+               recaptcha_response_field=None):
+        self.captcha = getUtility(ICaptcha, name='ReCaptcha')
+        result = self.captcha.verify(self.request)
+        self.captcha_error = result.error_code
+        print "VERIFY: ", result.is_valid, result.error_code
+        return
+
+    def render(self):
+        return """
+  <form method="POST" action="">
+    %s
+    <input type="submit" name="SUBMIT" />
+  </form>
+  """ % (self.captcha.display(self.captcha_error),)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/exceptions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/exceptions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/exceptions.py	(revision 16045)
@@ -0,0 +1,80 @@
+## $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
+##
+import grok
+import zope.errorview.browser
+from zope.interface.common.interfaces import IException
+from zope.errorview.http import SystemErrorViewMixin
+from zope.publisher.interfaces import INotFound
+from zope.security.interfaces import IUnauthorized
+from waeup.kofa.browser.layout import KofaPage
+
+grok.templatedir('templates')
+
+class ExceptionView(grok.View, zope.errorview.browser.ExceptionView,
+                    SystemErrorViewMixin):
+    """Base class for rendering views for uncaught exceptions that occur during
+    the application run-time and are not otherwise rendered.
+
+    Backport from Grok 1.6.
+
+    XXX: This view is shared for all apps and objects in the ZODB root.
+    """
+    grok.context(IException)
+    grok.name('index.html')
+    grok.template('exception')
+
+    def update(self):
+        return zope.errorview.browser.ExceptionView.update(self)
+
+class UnauthorizedView(grok.View, zope.errorview.browser.UnauthorizedView):
+    """Base class for rendering views for IUnauthorized exceptions.
+
+    Backport from grok 1.6. This is only a view (not a page) because
+    we cannot trust that the associated :exc:`Unauthorized` exceptions
+    happened inside a w.k. site.
+
+    If not, then we have no layout available at time of rendering.
+
+    XXX: This view is shared for all apps and objects in the ZODB root.
+    """
+    grok.context(IUnauthorized)
+    grok.name('index.html')
+    grok.template('unauthorized')
+
+    def update(self):
+        return zope.errorview.browser.UnauthorizedView.update(self)
+
+
+class NotFoundPage(KofaPage):
+    """A page rendered when an object cannot be found.
+
+    XXX: This page won't work for objects above a w.k.University.
+    """
+    grok.context(INotFound)
+    grok.name('index.html')
+    grok.template('notfound')
+
+    title = u'404: File Not Found'
+
+    def update(self):
+        try:
+            self.context = grok.getSite()
+        except:
+            pass
+        self.response.setStatus(404)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/fileviewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/fileviewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/fileviewlets.py	(revision 16045)
@@ -0,0 +1,206 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+
+import os
+import grok
+from zope.component import getUtility
+from zope.interface import Interface
+from waeup.kofa.interfaces import (
+    IKofaObject, IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser import DEFAULT_IMAGE_PATH
+from waeup.kofa.utils.helpers import (
+    string_from_bytes, file_size, get_fileformat)
+from waeup.kofa.browser.layout import (
+    default_filedisplay_template,
+    default_fileupload_template)
+
+ALLOWED_FILE_EXTENSIONS = ('jpg', 'png', 'pdf', 'tif', 'fpm')
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+
+def handle_file_delete(context, view, download_name):
+    """Handle deletion of file.
+
+    """
+    store = getUtility(IExtFileStore)
+    store.deleteFileByContext(context, attr=download_name)
+    context.writeLogMessage(view, 'deleted: %s' % download_name)
+    view.flash(_('${a} deleted.', mapping={'a': download_name}))
+    return
+
+
+def handle_file_upload(upload, context, view, max_size, download_name=None):
+    """Handle upload of file.
+
+    Returns `True` in case of success or `False`.
+
+    Please note that file pointer passed in (`upload`) most probably
+    points to end of file when leaving this function.
+    """
+    # Check some file requirements first
+    size = file_size(upload)
+    if size > max_size:
+        view.flash(_('Uploaded file is too big.'), type="danger")
+        return False
+    upload.seek(0)  # file pointer moved when determining size
+    dummy,ext = os.path.splitext(upload.filename)
+    # fpm files are expected to be fingerprint minutiae, file
+    # format is not yet checked
+    if ext == '.fpm':
+        file_format = 'fpm'
+    else:
+        file_format = get_fileformat(None, upload.read(512))
+        upload.seek(0)  # same here
+    if file_format is None:
+        view.flash(_('Could not determine file type.'), type="danger")
+        return False
+    basename, expected_ext = os.path.splitext(download_name)
+    if expected_ext:
+        if '.' + file_format != expected_ext:
+            view.flash(_('${a} file format expected.',
+                mapping={'a': expected_ext[1:]}), type="danger")
+            return False
+    else:
+        if not file_format in ALLOWED_FILE_EXTENSIONS:
+            view.flash(
+                _('Only the following extensions are allowed: ${a}',
+                mapping={'a': ', '.join(ALLOWED_FILE_EXTENSIONS)}),
+                type="danger")
+            return False
+        download_name += '.' + file_format
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName(attr=download_name)
+    store.createFile(file_id, upload)
+    context.writeLogMessage(view, 'uploaded: %s (%s)' % (
+        download_name,upload.filename))
+    view.flash(_('File ${a} uploaded.', mapping={'a': download_name}))
+    return True
+
+
+class FileManager(grok.ViewletManager):
+    """Viewlet manager for uploading files, preferably scanned images.
+    """
+    grok.name('files')
+
+
+class FileDisplay(grok.Viewlet):
+    """Base file display viewlet.
+    """
+    grok.baseclass()
+    grok.viewletmanager(FileManager)
+    template = default_filedisplay_template
+    label = _(u'File')
+    title = _(u'Scan')
+
+    @property
+    def download_filename(self):
+        return self.download_name
+
+    @property
+    def file_exists(self):
+        image = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        if image:
+            return True
+        else:
+            return False
+
+
+class FileUpload(FileDisplay):
+    """Base upload viewlet.
+    """
+    grok.baseclass()
+    grok.viewletmanager(FileManager)
+    template = default_fileupload_template
+    tab_redirect = '#tab2-top'
+    mus = 1024 * 150
+    upload_button =_('Upload selected file')
+    delete_button = _('Delete')
+
+    @property
+    def show_viewlet(self):
+        return True
+
+    @property
+    def input_name(self):
+        return "%s" % self.__name__
+
+    def update(self):
+        self.max_upload_size = string_from_bytes(self.mus)
+        delete_button = self.request.form.get(
+            'delete_%s' % self.input_name, None)
+        upload_button = self.request.form.get(
+            'upload_%s' % self.input_name, None)
+        if delete_button:
+            handle_file_delete(
+                context=self.context, view=self.view,
+                download_name=self.download_name)
+            self.view.redirect(
+                self.view.url(
+                    self.context, self.view.__name__) + self.tab_redirect)
+            return
+        if upload_button:
+            upload = self.request.form.get(self.input_name, None)
+            if upload:
+                # We got a fresh upload
+                handle_file_upload(upload,
+                    self.context, self.view, self.mus, self.download_name)
+                self.view.redirect(
+                    self.view.url(
+                        self.context, self.view.__name__) + self.tab_redirect)
+            else:
+                self.view.flash(_('No local file selected.'), type="danger")
+                self.view.redirect(
+                    self.view.url(
+                        self.context, self.view.__name__) + self.tab_redirect)
+        return
+
+
+class Image(grok.View):
+    """Renders images.
+    """
+    grok.baseclass()
+
+    @property
+    def download_filename(self):
+        return self.download_name
+
+    def render(self):
+        # A filename chooser turns a context into a filename suitable
+        # for file storage.
+        image = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        if image is None:
+            # show placeholder image
+            self.response.setHeader('Content-Type', 'image/jpeg')
+            return open(DEFAULT_IMAGE_PATH, 'rb').read()
+        dummy,ext = os.path.splitext(image.name)
+        if ext == '.jpg':
+            self.response.setHeader('Content-Type', 'image/jpeg')
+        elif ext == '.fpm':
+            self.response.setHeader('Content-Type', 'application/binary')
+        elif ext == '.png':
+            self.response.setHeader('Content-Type', 'image/png')
+        elif ext == '.tif':
+            self.response.setHeader('Content-Type', 'image/tiff')
+        elif ext == '.pdf':
+            self.response.setHeader('Content-Type', 'application/pdf')
+            self.response.setHeader('Content-Disposition',
+                'attachment; filename="%s.pdf' % self.download_filename)
+        return image
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/interfaces.py	(revision 16045)
@@ -0,0 +1,196 @@
+## $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
+##
+"""Interfaces for viewing components.
+"""
+from zope import schema
+from zope.interface import Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaObject, IUniversity, IUsersContainer, IDataCenter, validate_email)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.university.interfaces import (
+    IFacultiesContainer, IFaculty, IDepartment,
+    ICoursesContainer, ICourse, ICertificatesContainer,
+    ICertificate, ICertificateCourse)
+
+class IBreadcrumb(Interface):
+    """Provide breadcrumbs.
+    """
+    title = Attribute("Context description")
+    viewname = Attribute("The viewname, for which the breadcrumb is provided.")
+    parent_viewname = Attribute("The viewname of the parent to use.")
+    parent = Attribute("The parent object.")
+    context = Attribute("The context of the breadcrumb.")
+    target = Attribute("The link target.")
+
+class IBreadcrumbIgnorable(Interface):
+    """A marker interface for breadcrumbs that should be skipped in output.
+
+    If a breadcrumb wants to be skipped in real output (for instance,
+    because it is set on a layer in site hierarchy that should not be
+    accessed by users), it can also provide this interface. The
+    getBreadcrumbList() function defined here will exclude IIgnorables
+    by default.
+    """
+    pass
+
+class IBreadcrumbContainer(Interface):
+    """A container of breadcrumbs.
+    """
+    def __iter__():
+        """Allow iteration over the container.
+        """
+
+    def getList():
+        """Get the list of breadcrumbs as real Python list.
+        """
+class ICaptchaRequest(Interface):
+    """A set of data required to verify captcha solutions.
+
+    To solve a captcha we need at least a solution. Many types of
+    captchas might also need a challenge to compare whether the
+    solution is correct.
+    """
+    solution = schema.TextLine(
+        title = u'Solution string a user entered',
+        default = None,
+        )
+    challenge = schema.TextLine(
+        title = u'The challenge the solution might match',
+        default = None,
+        )
+
+class ICaptchaResponse(Interface):
+    """A formalized response for captcha solutions.
+    """
+    is_valid = schema.Bool(
+        title = u'Indicates validity of entered captcha data.',
+        default = False,
+        )
+    error_code = schema.TextLine(
+        title = u'Error when trying to validate entered captcha data.',
+        description = u'Error codes are not expected to be readable strings.',
+        default = None,
+        )
+
+class ICaptcha(Interface):
+
+    def verify(request):
+        """Verify data entered in an HTTP request.
+
+        Expects some IHTTPRequest object and returns an
+        ICaptchaResponse indicating that the solution was correct or
+        not.
+
+        If the solution could not be verified (this might also happen
+        because of technical reasons), the response might contain an
+        error code.
+        """
+
+    def display(error_code=None):
+        """Returns a piece of HTML code that displays the captcha.
+
+        The generated HTML code might depend on any previous error
+        code as returned from :meth:`verify`.
+
+        The code is expected to appear inside a ``<form>``. It
+        therefore should not contain a ``<form>`` nor any submit
+        buttons.
+        """
+
+class ICaptchaConfig(Interface):
+    """Any type of captcha might need some configuration data.
+
+    By default we require no configuration data.
+    """
+
+class ICaptchaManager(Interface):
+    """A chooser that collects available captchas.
+    """
+    def getAvailCaptchas():
+        """Return a dict of available captchas with registered names as keys.
+        """
+
+    def getCaptcha():
+        """Get captcha chosen for a certain site or default.
+        """
+
+class IPDFCreator(Interface):
+    """A component that knows where logo graphics for PDFs are stored
+    and can generate PDF documents.
+
+    It is a utility (and not a simple function or class) to make these
+    data customizable in derived packages.
+    """
+    header_logo_path = schema.TextLine(
+        title = u'Path to header logo JPG')
+    watermark_path = schema.TextLine(
+        title = u'Path to watermark logo JPG')
+    def paint_background(canvas, doc):
+        """A callback function to render background of PDFs.
+        """
+    def create_pdf(data, headerline=None, title=None, author=None,
+                   footer='', note=None, sigs_in_footer=[]):
+        """Create a PDF.
+
+        `data` is expected to be a list of reportlab flowables
+        (paragraphs, images, tables, etc.), that will be rendered into
+        some background.
+
+        `headerline` will be displayed in page head and `title` under
+        the top bar.
+
+        `note` is optional HTML markup added at bottom of created
+        document.
+
+        `footer` is the text rendered in the bottom line next to the
+        page numbers.
+
+        `sigs_in_footer` is a set of translateable strings that will be
+        rendered into signature boxes at bottom of each page.
+
+        If no `headerline` is given, a default will be rendered (name
+        of university).
+
+        If no `title` is given, nothing will be rendered.
+        """
+
+class IChangePassword(IKofaObject):
+    """Interface needed for change pasword page.
+
+    """
+    identifier = schema.TextLine(
+        title = _(u'Unique Identifier'),
+        description = _(
+            u'User Name, Student or Applicant Id, Matriculation or '
+            u'Registration Number'),
+        required = True,
+        readonly = False,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+class IStudentNavigationBase(IKofaObject):
+    """Objects that provide student navigation should
+       implement this interface.
+    """
+    student = Attribute('''Some student object that has a '''
+                        ''' `display_fullname` attribute.''')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/layout.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/layout.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/layout.py	(revision 16045)
@@ -0,0 +1,320 @@
+## $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
+##
+"""Basic layout components.
+"""
+import os
+import grok
+from datetime import date, datetime
+from grokcore.view import PageTemplateFile
+from grokcore.formlib.formlib import Action
+from cgi import escape
+from zope.i18n import translate
+from zope.i18nmessageid import Message
+from megrok.layout import Page, Layout, Form, EditForm, DisplayForm, AddForm
+from z3c.flashmessage.interfaces import IMessageSource, IMessageReceiver
+from zope.component import getUtility, queryUtility, ComponentLookupError
+from zope.formlib.utility import setUpWidgets
+from zope.interface import Interface
+from zope.site.hooks import getSite
+from waeup.kofa.interfaces import IKofaObject, IUserAccount, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.browser.interfaces import IStudentNavigationBase
+from waeup.kofa.authentication import get_principal_role_manager
+
+grok.templatedir('templates')
+default_waeup_display_template = PageTemplateFile(
+    os.path.join('templates', 'default_waeup_display_form.pt'))
+default_waeup_display_template.__grok_name__ = 'default_waeup_display_form'
+
+default_waeup_edit_template = PageTemplateFile(
+    os.path.join('templates', 'default_waeup_edit_form.pt'))
+default_waeup_edit_template.__grok_name__ = 'default_waeup_edit_form'
+
+default_primary_nav_template = PageTemplateFile(
+    os.path.join('templates', 'primarynavtab.pt'))
+default_primary_nav_template.__grok_name__ = 'default_primary_nav'
+
+default_filedisplay_template = PageTemplateFile(
+    os.path.join('templates', 'filedisplay.pt'))
+default_filedisplay_template.__grok_name__ = 'default_filedisplay'
+
+default_fileupload_template = PageTemplateFile(
+    os.path.join('templates', 'fileupload.pt'))
+default_fileupload_template.__grok_name__ = 'default_fileupload'
+
+class action(grok.action):
+
+    def __call__(self, success):
+        action = KofaAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+class KofaAction(Action):
+
+    def __init__(self, label, style='', tooltip='', warning='', **options):
+        super(KofaAction, self).__init__(label, **options)
+        self.style = style
+        self.tooltip = tooltip
+        self.warning = warning
+        if style == '':
+            self.style = 'default'
+
+    def render(self):
+        if not self.available():
+            return ''
+        label = self.label
+        if isinstance(label, Message):
+            label = translate(self.label, context=self.form.request)
+        if self.warning:
+            warning = translate(self.warning, context=self.form.request)
+            self.warning = ' onclick="return window.confirm(\'%s\')"' % warning
+        if self.tooltip:
+            tooltip = translate(self.tooltip, context=self.form.request)
+            self.tooltip = ' data-toggle="tooltip" title="%s"' % tooltip
+        if self.style:
+            style = ' class="btn btn-%s"' % self.style
+        else:
+            style = ' class="btn btn-default"'
+        html = ('<input type="submit" id="%s" name="%s" value="%s"' %
+                (self.__name__, self.__name__, escape(label, quote=True))
+                + style + self.tooltip + self.warning
+                + ' />')
+        return html
+
+class jsaction(grok.action):
+
+    msg = _('Are you sure?')
+
+    def __call__(self, success):
+        action = KofaAction(self.label,
+                            success=success, warning=self.msg,
+                            **self.options)
+        self.actions.append(action)
+        return action
+
+def NullValidator(*args, **kw):
+    """A validator that does not validate.
+
+    This is needed especially for cancel buttons. We don't want data
+    to be validated that will be thrown away in the next step.
+
+    You can use it with ``grok.action`` decorator like this::
+
+      @grok.action('Cancel', validator=NullValidator)
+      def cancel(self, **data):
+        self.redirect(<whereever-you-go>)
+    """
+    return dict()
+
+class Messages(grok.View):
+    """Display messages of message receivers.
+    """
+
+    grok.context(Interface)
+
+    @property
+    def messages(self):
+        receiver = getUtility(IMessageReceiver)
+        return receiver.receive()
+
+class UtilityView(object):
+    """A view mixin with useful methods.
+
+    The ``pnav`` attribute (a number) tells, to which primary
+    navigation tab a page declares to belong.
+    """
+    title = u'' # What appears in the content title...
+    pnav = 0 # Primary navigation index...
+
+    def application_url(self, name=None):
+        """Return the URL of the nearest site.
+        """
+        site = getSite()
+        #if not site:
+        #    raise ComponentLookupError("No site found.")
+        return self.url(site, name)
+
+    def flash(self, message, type='success'):
+        """Send a short message to the user.
+        """
+        cssClass = 'alert alert-%s' % type
+        source = queryUtility(IMessageSource, name='session')
+        if source is None:
+            return None
+        source.send(message, cssClass)
+        return True
+
+class KofaLayout(UtilityView,Layout):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+
+class KofaPage(UtilityView,Page):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+class KofaFormPage(UtilityView,Form):
+    """A megrok.layout.Form with additional methods.
+    """
+    grok.baseclass()
+
+    def setUpWidgets(self,ignore_request=False):
+        super(KofaFormPage,self).setUpWidgets(ignore_request)
+        # Width parameters will be overridden by Bootstrap
+        # so we have to set the css class
+        if self.widgets.get('body'):
+            self.widgets['body'].height = 10
+
+class KofaDisplayFormPage(UtilityView,DisplayForm):
+    """A megrok.layout.DisplayForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_display_template
+    hide_hint = True
+
+class KofaEditFormPage(UtilityView,EditForm):
+    """A megrok.layout.EditForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+    def setUpWidgets(self,ignore_request=False):
+        super(KofaEditFormPage,self).setUpWidgets(ignore_request)
+        for widget in self.widgets:
+            if 'address' in widget.name or \
+                'comment' in widget.name or \
+                'message' in widget.name or \
+                'notice' in widget.name:
+                widget.height = 6
+        if self.widgets.get('transcript_comment'):
+            self.widgets['transcript_comment'].height = 12
+        if self.widgets.get('jamb_subjects'):
+            self.widgets['jamb_subjects'].height = 6
+
+class KofaAddFormPage(UtilityView,AddForm):
+    """A megrok.layout.AddForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+class SiteLayout(KofaLayout):
+    """ The general site layout.
+    """
+    grok.context(IKofaObject)
+
+    #: An instance of the default theme to use for the site layout
+    stafftemp = grok.PageTemplateFile('templates/staffsitelayout.pt')
+    studenttemp = grok.PageTemplateFile('templates/studentsitelayout.pt')
+
+    @property
+    def site(self):
+        return grok.getSite()
+
+    def getAppTitle(self):
+        return getattr(grok.getSite()['configuration'], 'name', u'Sample University')
+
+    def getAppAcronym(self):
+        return getattr(grok.getSite()['configuration'], 'acronym', u'Acronym')
+
+    def isAuthenticated(self):
+        """Return True if the calling user is authenticated.
+        """
+        usertitle = self.request.principal.title
+        return usertitle != 'Unauthenticated User'
+
+    def isMaintenanceMode(self):
+        """Return True if portal is in maintenance mode.
+        """
+        if grok.getSite()['configuration'].maintmode_enabled_by:
+            return True
+        return False
+
+    def getUserTitle(self):
+        """Return principal title of current user.
+        """
+        usertitle = self.request.principal.title
+        if usertitle == 'Unauthenticated User':
+            return u'Anonymous User'
+        return usertitle
+
+    def getUserId(self):
+        """Return id of current user.
+        """
+        userid = self.request.principal.id
+        return userid
+
+    def isStudent(self):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        return self.request.principal.user_type == 'student'
+
+    def isApplicant(self):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        return self.request.principal.user_type == 'applicant'
+
+    def getStudentName(self):
+        """Return the student name.
+        """
+        if IStudentNavigationBase.providedBy(self.context):
+            return self.context.student.display_fullname
+        return
+
+    def formatDatetime(self,datetimeobj):
+        if isinstance(datetimeobj, datetime):
+            tz = getUtility(IKofaUtils).tzinfo
+            try:
+                timestamp = to_timezone(
+                    datetimeobj, tz).strftime("%Y-%m-%d %H:%M:%S")
+            except ValueError:
+                return None
+            return timestamp
+        else:
+            return None
+
+    def formatDate(self,dateobj):
+        if isinstance(dateobj, date):
+            return dateobj.strftime("%d/%m/%Y")
+        else:
+            return None
+            
+    def formatTZDate(self,datetimeobj):
+        if isinstance(datetimeobj, datetime):
+            if datetimeobj.year == 2099:
+                return _('OE')
+            tz = getUtility(IKofaUtils).tzinfo
+            date = to_timezone(
+                datetimeobj, tz).strftime("%d/%m/%Y")
+            return date
+        else:
+            return None          
+
+    @property
+    def course_cat_used(self):
+        if len(getUtility(IKofaUtils).COURSE_CATEGORY_DICT):
+            return True
+        return False
+
+    def render(self):
+        if self.isStudent() or self.isApplicant() or not self.isAuthenticated():
+            return self.studenttemp.render(self)
+        return self.stafftemp.render(self)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pages.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pages.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pages.py	(revision 16045)
@@ -0,0 +1,2864 @@
+## $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
+##
+""" Viewing components for Kofa objects.
+"""
+# XXX: All csv ops should move to a dedicated module soon
+import cgi
+import unicodecsv as csv
+import grok
+import os
+import re
+import sys
+from datetime import datetime, timedelta
+from urllib import urlencode
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope import schema
+from zope.i18n import translate
+from zope.authentication.interfaces import (
+    IAuthentication, IUnauthenticatedPrincipal, ILogout)
+from zope.catalog.interfaces import ICatalog
+from zope.component import (
+    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
+    getUtilitiesFor,
+    )
+from zope.event import notify
+from zope.security import checkPermission
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.session.interfaces import ISession
+from zope.password.interfaces import IPasswordManager
+from waeup.kofa.utils.helpers import html2dict
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaFormPage, KofaEditFormPage, KofaAddFormPage,
+    KofaDisplayFormPage, NullValidator)
+from waeup.kofa.browser.interfaces import (
+    IUniversity, IFacultiesContainer, IFaculty,
+    IDepartment, ICourse, ICertificate,
+    ICertificateCourse,
+    ICaptchaManager, IChangePassword)
+from waeup.kofa.browser.layout import jsaction, action, UtilityView
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import(
+    IKofaObject, IUsersContainer, IUserAccount, IDataCenter,
+    IBatchProcessor,
+    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
+    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
+    IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,
+    academic_sessions_vocab, DOCLINK, registration_states_vocab,
+    TRANSREQ, TRANSVAL, NotAnEmailAddress)
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.university.course import Course
+from waeup.kofa.permissions import (
+    get_users_with_local_roles, get_all_roles,
+    get_users_with_role)
+
+from waeup.kofa.university.catalog import search
+from waeup.kofa.university.vocabularies import course_levels
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.utils.helpers import get_user_account, check_csv_charset
+from waeup.kofa.mandates.mandate import PasswordMandate
+from waeup.kofa.datacenter import DataCenterFile
+
+from waeup.kofa.applicants.interfaces import IApplicantsUtils
+
+from waeup.kofa.students.catalog import StudentQueryResultItem
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.vocabularies import StudyLevelSource, GenderSource
+
+FORBIDDEN_CHARACTERS = (160,)
+
+grok.context(IKofaObject)
+grok.templatedir('templates')
+
+def add_local_role(view, tab, **data):
+    localrole = view.request.form.get('local_role', None)
+    user = view.request.form.get('user', None)
+    if user is None or localrole is None:
+        view.flash('No user selected.', type='danger')
+        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
+        return
+    role_manager = IPrincipalRoleManager(view.context)
+    role_manager.assignRoleToPrincipal(localrole, user)
+    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
+    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+    grok.getSite().logger.info(
+        '%s - added: %s|%s' % (ob_class, user, localrole))
+    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
+    return
+
+def del_local_roles(view, tab, **data):
+    child_ids = view.request.form.get('role_id', None)
+    if child_ids is None:
+        view.flash(_('No local role selected.'), type='danger')
+        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
+        return
+    if not isinstance(child_ids, list):
+        child_ids = [child_ids]
+    deleted = []
+    role_manager = IPrincipalRoleManager(view.context)
+    for child_id in child_ids:
+        localrole = child_id.split('|')[1]
+        user_name = child_id.split('|')[0]
+        try:
+            role_manager.unsetRoleForPrincipal(localrole, user_name)
+            notify(LocalRoleSetEvent(
+                    view.context, localrole, user_name, granted=False))
+            deleted.append(child_id)
+        except:
+            view.flash('Could not remove %s: %s: %s' % (
+                    child_id, sys.exc_info()[0], sys.exc_info()[1]),
+                    type='danger')
+    if len(deleted):
+        view.flash(
+            _('Local role successfully removed: ${a}',
+            mapping = {'a':', '.join(deleted)}))
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        grok.getSite().logger.info(
+            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
+    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
+    return
+
+def delSubobjects(view, redirect, tab=None, subcontainer=None):
+    form = view.request.form
+    if 'val_id' in form:
+        child_id = form['val_id']
+    else:
+        view.flash(_('No item selected.'), type='danger')
+        if tab:
+            view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
+        else:
+            view.redirect(view.url(view.context, redirect))
+        return
+    if not isinstance(child_id, list):
+        child_id = [child_id]
+    deleted = []
+    for id in child_id:
+        try:
+            if subcontainer:
+                container = getattr(view.context, subcontainer, None)
+                del container[id]
+            else:
+                del view.context[id]
+            deleted.append(id)
+        except:
+            view.flash('Could not delete %s: %s: %s' % (
+                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
+    if len(deleted):
+        view.flash(_('Successfully removed: ${a}',
+            mapping = {'a': ', '.join(deleted)}))
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        grok.getSite().logger.info(
+            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
+    if tab:
+        view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
+    else:
+        view.redirect(view.url(view.context, redirect))
+    return
+
+def getPreviewTable(view, n):
+    """Get transposed table with n sample record.
+
+    The first column contains the headers.
+    """
+    if not view.reader:
+        return
+    header = view.getPreviewHeader()
+    num = 0
+    data = []
+    for line in view.reader:
+        if num > n-1:
+            break
+        num += 1
+        data.append(line)
+    result = []
+    for name in header:
+        result_line = []
+        result_line.append(name)
+        for d in data:
+            result_line.append(d[name])
+        result.append(result_line)
+    return result
+
+# 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 - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
+    return
+
+def doll_up(view, user=None, purge=False):
+    """Doll up export jobs for displaying in table.
+    """
+    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+    job_entries = view.context.get_running_export_jobs(user)
+    job_manager = getUtility(IJobManager)
+    entries = []
+    for job_id, exporter_name, user_id in job_entries:
+        job = job_manager.get(job_id)
+        exporter = getUtility(ICSVExporter, name=exporter_name)
+        exporter_title = getattr(exporter, 'title', 'Unknown')
+        if not job.kwargs.has_key('selected'):
+            args = ', '.join(['%s=%s' % (item[0], item[1])
+                for item in job.kwargs.items()
+                if item[1] not in ('all', '', None)])
+        else:
+            args = 'selection'
+        status = job.finished and 'ready' or 'running'
+        status = job.failed and 'FAILED' or status
+        start_time = getattr(job, 'begin_after', None)
+        time_delta = None
+        if start_time:
+            tz = getUtility(IKofaUtils).tzinfo
+            time_delta = datetime.now(tz) - start_time
+            start_time = start_time.astimezone(tz).strftime(
+                "%Y-%m-%d %H:%M:%S %Z")
+        download_url = view.url(view.context, 'download_export',
+                                data=dict(job_id=job_id))
+        show_download_button = job.finished and not \
+                               job.failed and time_delta and \
+                               time_delta.days < 1
+        if not show_download_button and purge:
+            entry = view.context.entry_from_job_id(job_id)
+            view.context.delete_export_entry(entry)
+            view.context.logger.info(
+                '%s - purged: job_id=%s' % (ob_class, job_id))
+            continue
+        new_entry = dict(
+            job=job_id,
+            creator=user_id,
+            args=args,
+            exporter=exporter_title,
+            status=status,
+            start_time=start_time,
+            download_url=download_url,
+            show_download_button = show_download_button,
+            show_refresh_button = not job.finished,
+            show_discard_button = job.finished,)
+        entries.append(new_entry)
+    return entries
+
+def running_jobs(view):
+    """True if any job is running.
+    """
+    job_entries = view.context.get_running_export_jobs()
+    job_manager = getUtility(IJobManager)
+    entries = []
+    for entry in job_entries:
+        job = job_manager.get(entry[0])
+        status = job.finished and 'ready' or 'running'
+        status = job.failed and 'FAILED' or status
+        if status == 'running':
+            return True
+    return False
+
+def exports_not_allowed(view):
+    """Return flash message if exports are not allowed.
+    """
+    utils = queryUtility(IKofaUtils)
+    if utils.export_disabled_message():
+        view.entries = doll_up(view, user=None)
+        return utils.export_disabled_message()
+    if not utils.expensive_actions_allowed():
+        view.entries = doll_up(view, user=None)
+        return _("Currently, exporters cannot be started due to high "
+                 "system load. Please try again later.")
+    if running_jobs(view):
+        view.entries = doll_up(view, user=None)
+        return _("Sorry, another export job is running. "
+                 "Please try again later.")
+    return
+
+def getImporters(context):
+    importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+    importer_props = []
+    for x in importers:
+        # Skip User Processor if user isn't allowed to manage users.
+        if x.util_name == 'userprocessor' and not checkPermission(
+            'waeup.manageUsers', context):
+            continue
+        iface_fields = schema.getFields(x.iface)
+        available_fields = []
+        for key in iface_fields.keys():
+            iface_fields[key] = (iface_fields[key].__class__.__name__,
+                iface_fields[key].required)
+        for value in x.available_fields:
+            available_fields.append(
+                dict(f_name=value,
+                     f_type=iface_fields.get(value, (None, False))[0],
+                     f_required=iface_fields.get(value, (None, False))[1]
+                     )
+                )
+        available_fields = sorted(available_fields, key=lambda k: k[
+            'f_name'])
+        importer_props.append(
+            dict(title=x.name, name=x.util_name, fields=available_fields))
+    return sorted(importer_props, key=lambda k: k['title'])
+
+class LocalRoleAssignmentUtilityView(object):
+    """A view mixin with useful methods for local role assignment.
+
+    """
+
+    def getLocalRoles(self):
+        roles = ILocalRolesAssignable(self.context)
+        return roles()
+
+    def getUsers(self):
+        return getUtility(IKofaUtils).getUsers()
+
+    def getUsersWithLocalRoles(self):
+        return get_users_with_local_roles(self.context)
+
+#
+# Login/logout and language switch pages...
+#
+
+class LoginPage(KofaPage):
+    """A login page, available for all objects.
+    """
+    grok.name('login')
+    grok.context(IKofaObject)
+    grok.require('waeup.Public')
+    label = _(u'Login')
+    camefrom = None
+    login_button = label
+
+    def _comment(self, student):
+        return getattr(student, 'suspended_comment', None)
+
+    def update(self, SUBMIT=None, camefrom=None):
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode '
+                        'and authentication (login) temporarily disabled.'),
+                       type='warning')
+        self.camefrom = camefrom
+        if SUBMIT is not None:
+            if self.request.principal.id != 'zope.anybody':
+                self.flash(_('You logged in.'))
+                if self.request.principal.user_type == 'student':
+                    student = grok.getSite()['students'][
+                        self.request.principal.id]
+                    rel_link = '/students/%s' % self.request.principal.id
+                    if student.getParentsPassword():
+                        # Add logging message
+                        student.writeLogMessage(self, 'Parents logged in')
+                        # Replace role if parents have logged in
+                        role_manager = IPrincipalRoleManager(student)
+                        role_manager.removeRoleFromPrincipal(
+                            'waeup.local.StudentRecordOwner', student.student_id)
+                        notify(LocalRoleSetEvent(
+                            student, 'waeup.local.StudentRecordOwner',
+                            student.student_id, granted=False))
+                        role_manager.assignRoleToPrincipal(
+                            'waeup.local.Parents', student.student_id)
+                        notify(LocalRoleSetEvent(
+                            student, 'waeup.local.Parents',
+                            student.student_id, granted=True))
+                    else:
+                        # Redirect to personal page if data are outdated
+                        if student.personal_data_expired:
+                            rel_link = '/students/%s/edit_personal' % (
+                                self.request.principal.id)
+                            self.flash(
+                              _('Your personal data record is outdated. Please update.'),
+                              type='warning')
+                    self.redirect(self.application_url() + rel_link)
+                    return
+                elif self.request.principal.user_type == 'applicant':
+                    container, application_number = self.request.principal.id.split('_')
+                    rel_link = '/applicants/%s/%s' % (
+                        container, application_number)
+                    self.redirect(self.application_url() + rel_link)
+                    return
+                # Redirect some officers after login
+                local_roles = get_user_account(self.request).getLocalRoles()
+                if local_roles.has_key('waeup.local.Lecturer'):
+                    self.redirect(self.application_url() + '/my_courses')
+                    return
+                if local_roles.has_key('waeup.local.TranscriptOfficer'):
+                    self.redirect(self.application_url() + '/my_transcript_requests')
+                    return
+                if local_roles.has_key('waeup.local.TranscriptSignee'):
+                    self.redirect(self.application_url() + '/my_transcript_signing_requests')
+                    return
+                if not self.camefrom:
+                    self.redirect(self.application_url() + '/index')
+                    return
+                self.redirect(self.camefrom)
+                return
+            # Display second warning message and remind user that
+            # s/he can't login.
+            if blocker:
+                self.flash(_('The portal is in maintenance mode. '
+                            'You can\'t login!'),
+                           type='danger')
+                return
+            # Display appropriate flash message if credentials are correct
+            # but student has been deactivated or a temporary or parents 
+            # password has been set.
+            login = self.request.form['form.login']
+            if len(login) == 8 and login in grok.getSite()['students']:
+                student = grok.getSite()['students'][login]
+                password = self.request.form['form.password']
+                passwordmanager = getUtility(IPasswordManager, 'SSHA')
+                if student.password is not None and \
+                    passwordmanager.checkPassword(student.password, password):
+                    # The student entered valid credentials.
+                    # First we check if a temporary password has been set.
+                    if student.getTempPassword():
+                        self.flash(
+                            _('Your account has been temporarily deactivated.'),
+                            type='warning')
+                        return
+                    # Next we check if a parents password has been set.
+                    if student.getParentsPassword():
+                        self.flash(
+                            _('Your account has been temporarily deactivated '
+                              'because your parents have logged in.'),
+                            type='warning')
+                        return
+                    # Now we know that the student is suspended.
+                    comment = self._comment(student)
+                    if comment:
+                        self.flash(comment, type='warning')
+                    else:
+                        self.flash(_('Your account has been deactivated.'),
+                                   type='warning')
+                    return
+            # Display appropriate flash message if credentials are correct
+            # but the stored password is insecure or officer has been
+            # deactivated.
+            login = self.request.form['form.login']
+            if login in grok.getSite()['users']:
+                user = grok.getSite()['users'][login]
+                password = self.request.form['form.password']
+                passwordmanager = getUtility(IPasswordManager, 'SSHA')
+                if user.password is not None and \
+                    passwordmanager.checkPassword(user.password, password):
+                    # Check first if the stored password might have
+                    # been the reason
+                    validator = getUtility(IPasswordValidator)
+                    errors = validator.validate_secure_password(
+                        password, password)
+                    if errors:
+                        msg1 = translate(_(
+                                 'Your user name and password are correct '
+                                 'but your password is not considered '
+                                 'secure. '))
+                        msg2 = translate(_(
+                                 ' Your account has been temporarily '
+                                 'deactivated. '
+                                 'Please request a new password.'))
+                        self.flash( msg1 + ' '.join(errors) + msg2, type="danger")
+                        self.redirect(self.application_url() + '/changepw')
+                        return
+                    self.flash(_('Your user name and password are correct '
+                                 'but your account has been temporarily '
+                                 'deactivated.'),
+                               type='warning')
+                    return
+            self.flash(_('You entered invalid credentials.'), type='danger')
+            return
+
+
+class LogoutPage(KofaPage):
+    """A logout page. Calling this page will log the current user out.
+    """
+    grok.context(IKofaObject)
+    grok.require('waeup.Public')
+    grok.name('logout')
+
+    def update(self):
+        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
+            auth = getUtility(IAuthentication)
+            ILogout(auth).logout(self.request)
+            self.flash(_("You have been logged out. Thanks for using WAeUP Kofa!"))
+        self.redirect(self.application_url() + '/index')
+        return
+
+
+class LanguageChangePage(KofaPage):
+    """ Language switch
+    """
+    grok.context(IKofaObject)
+    grok.name('change_language')
+    grok.require('waeup.Public')
+
+    def update(self, lang='en', view_name='@@index'):
+        self.response.setCookie('kofa.language', lang, path='/')
+        self.redirect(self.url(self.context, view_name))
+        return
+
+    def render(self):
+        return
+
+#
+# Contact form...
+#
+
+class ContactAdminFormPage(KofaFormPage):
+    grok.name('contactadmin')
+    #grok.context(IUniversity)
+    grok.template('contactform')
+    grok.require('waeup.Authenticated')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select('body')
+
+    def update(self):
+        super(ContactAdminFormPage, self).update()
+        self.form_fields.get('body').field.default = None
+        return
+
+    @property
+    def config(self):
+        return grok.getSite()['configuration']
+
+    def label(self):
+        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
+
+    @property
+    def get_user_account(self):
+        return get_user_account(self.request)
+
+    @action(_('Send message now'), style='primary')
+    def send(self, *args, **data):
+        fullname = self.request.principal.title
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        username = self.request.principal.id
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        kofa_utils = getUtility(IKofaUtils)
+        success = kofa_utils.sendContactForm(
+                fullname,email,
+                self.config.name_admin,self.config.email_admin,
+                username,usertype,self.config.name,
+                data['body'],self.config.email_subject)
+        # Success is always True if sendContactForm didn't fail.
+        # TODO: Catch exceptions.
+        if success:
+            self.flash(_('Your message has been sent.'))
+        return
+
+class EnquiriesFormPage(ContactAdminFormPage):
+    """Captcha'd page to let anonymous send emails to the administrator.
+    """
+    grok.name('enquiries')
+    grok.require('waeup.Public')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select(
+                          'fullname', 'email_from', 'body')
+
+    def update(self):
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode. '
+                        'The contact form is temporarily disabled.'),
+                       type='warning')
+            self.redirect(self.url(self.context))
+            return
+        super(EnquiriesFormPage, self).update()
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    @action(_('Send now'), style='primary')
+    def send(self, *args, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        kofa_utils = getUtility(IKofaUtils)
+        success = kofa_utils.sendContactForm(
+                data['fullname'],data['email_from'],
+                self.config.name_admin,self.config.email_admin,
+                u'None',u'Anonymous',self.config.name,
+                data['body'],self.config.email_subject)
+        if success:
+            self.flash(_('Your message has been sent.'))
+        else:
+            self.flash(_('A smtp server error occurred.'), type='danger')
+        return
+
+#
+# University related pages...
+#
+
+class UniversityPage(KofaDisplayFormPage):
+    """ The main university page.
+    """
+    grok.require('waeup.Public')
+    grok.name('index')
+    grok.context(IUniversity)
+    pnav = 0
+    label = ''
+
+    @property
+    def frontpage(self):
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context['configuration'].frontpage_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context[
+                'configuration'].frontpage_dict.get(portal_language,'')
+        if html =='':
+            return _(u'<h1>Welcome to WAeUP.Kofa</h1>')
+        else:
+            return html
+
+class SourcesOverview(grok.View):
+    grok.context(IUniversity)
+    grok.name('sourcesoverview')
+    grok.require('waeup.manageDataCenter')
+
+    def _set_inst_types(self):
+        study_modes = getUtility(IKofaUtils).INST_TYPES_DICT
+        self.inst_types = sorted(study_modes.items())
+        return
+
+    def _set_sessions(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(x.token, x.title,) for x in vocab_terms]
+        return
+
+    def _set_course_levels(self):
+        vocab_terms = course_levels.by_value.values()
+        self.course_levels = sorted([(x.token, x.title,) for x in vocab_terms])
+        return
+
+    def _set_study_levels(self):
+        studylevelsource = StudyLevelSource().factory
+        self.study_levels = []
+        for code in studylevelsource.getValues(None):
+            title = studylevelsource.getTitle(None, code)
+            self.study_levels.append((code, title))
+        return
+
+    def _set_study_modes(self):
+        study_modes = getUtility(IKofaUtils).STUDY_MODES_DICT
+        self.study_modes = sorted(study_modes.items())
+        return
+
+    def _set_degrees(self):
+        degrees = getUtility(IKofaUtils).DEGREES_DICT
+        self.degrees = sorted(degrees.items())
+        return
+
+    def _set_student_states(self):
+        vocab_terms = registration_states_vocab.by_value.values()
+        self.student_states = [(x.token, x.title) for x in vocab_terms]
+        return
+
+    def _set_verdicts(self):
+        verdicts = getUtility(IKofaUtils).VERDICTS_DICT
+        self.verdicts = sorted(verdicts.items())
+        return
+
+    def _set_exam_subjects(self):
+        exam_subjects = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
+        self.exam_subjects = sorted(exam_subjects.items())
+        return
+
+    def _set_exam_grades(self):
+        self.exam_grades = getUtility(IKofaUtils).EXAM_GRADES
+        return
+
+    def _set_app_cats(self):
+        app_cats = getUtility(IKofaUtils).APP_CATS_DICT
+        self.app_cats = sorted(app_cats.items())
+        return
+
+    def _set_app_types(self):
+        app_types = getUtility(IApplicantsUtils).APP_TYPES_DICT
+        self.app_types = sorted([(x[0], x[1][0]) for x in app_types.items()])
+        return
+
+    def _set_payment_cats(self):
+        payment_cats = getUtility(IKofaUtils).PAYMENT_CATEGORIES
+        self.payment_cats = sorted(payment_cats.items())
+        return
+
+    def _set_sex(self):
+        gendersource = GenderSource().factory
+        self.sex = []
+        for sex in gendersource.getValues():
+            title = gendersource.getTitle(sex)
+            self.sex.append((sex, title))
+        return
+
+    def _set_disable_payment_groups(self):
+        disable_payment_groups = getUtility(
+            IKofaUtils).DISABLE_PAYMENT_GROUP_DICT
+        self.disable_payment_groups = sorted(disable_payment_groups.items())
+        return
+
+    def update(self):
+        self._set_inst_types()
+        self._set_sessions()
+        self._set_course_levels()
+        self._set_study_levels()
+        self._set_study_modes()
+        self._set_degrees()
+        self._set_student_states()
+        self._set_verdicts()
+        self._set_exam_subjects()
+        self._set_exam_grades()
+        self._set_app_cats()
+        self._set_app_types()
+        self._set_payment_cats()
+        self._set_sex()
+        self. _set_disable_payment_groups()
+        return
+
+class SourcesOverviewPage(KofaPage, SourcesOverview):
+    grok.name('sources')
+    grok.require('waeup.Public')
+    label = _(u'Sources & Vocabularies')
+    pnav = 0
+
+class ProcessorsOverview(grok.View):
+    grok.context(IUniversity)
+    grok.name('processorsoverview')
+    grok.require('waeup.manageDataCenter')
+
+    def getImporters(self):
+        return getImporters(self.context)
+
+class ProcessorsOverviewPage(KofaPage, ProcessorsOverview):
+    grok.name('processors')
+    grok.require('waeup.Public')
+    label = _(u'Available Processors (Importers)')
+    pnav = 0
+
+class SkeletonDownloadView(UtilityView, grok.View):
+    grok.context(IUniversity)
+    grok.name('skeleton')
+    grok.require('waeup.Public')
+
+    def update(self, processorname=None):
+        self.processorname = self.request.form['name']
+        self.filename = ('%s_000.csv' %
+            self.processorname.replace('processor','import'))
+        return
+
+    def render(self):
+        #ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        #self.context.logger.info(
+        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
+        processor = getUtility(IBatchProcessor, name=self.processorname)
+        csv_data = processor.get_csv_skeleton()
+        return csv_data
+
+class AdministrationPage(KofaPage):
+    """ The administration overview page.
+    """
+    grok.name('administration')
+    grok.context(IUniversity)
+    grok.require('waeup.managePortal')
+    label = _(u'Administration')
+    pnav = 0
+
+class RSS20Feed(grok.View):
+    """An RSS 2.0 feed.
+    """
+    grok.name('feed.rss')
+    grok.context(IUniversity)
+    grok.require('waeup.Public')
+    grok.template('universityrss20feed')
+
+    name = 'General news feed'
+    description = 'waeup.kofa now supports RSS 2.0 feeds :-)'
+    language = None
+    date = None
+    buildDate = None
+    editor = None
+    webmaster = None
+
+    @property
+    def title(self):
+        return getattr(grok.getSite(), 'name', u'Sample University')
+
+    @property
+    def contexttitle(self):
+        return self.name
+
+    @property
+    def link(self):
+        return self.url(grok.getSite())
+
+    def update(self):
+        self.response.setHeader('Content-Type', 'text/xml; charset=UTF-8')
+
+    def entries(self):
+        return ()
+
+#
+# User container pages...
+#
+
+class UsersContainerPage(KofaPage):
+    """Overview page for all local users.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.name('index')
+    label = _('Officers')
+    manage_button = _(u'Manage')
+    delete_button = _(u'Remove')
+    doclink = DOCLINK + '/users.html#officers'
+
+    def update(self, userid=None, adduser=None, manage=None, delete=None):
+        if manage is not None and userid is not None:
+            self.redirect(self.url(userid) + '/@@manage')
+        if delete is not None and userid is not None:
+            self.context.delUser(userid)
+            self.flash(_('User account ${a} successfully deleted.',
+                mapping = {'a':  userid}))
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.__parent__.logger.info(
+                '%s - removed: %s' % (ob_class, userid))
+
+    def getLocalRoles(self, account):
+        local_roles = account.getLocalRoles()
+        local_roles_string = ''
+        site_url = self.url(grok.getSite())
+        for local_role in local_roles.keys():
+            role_title = getattr(
+                dict(get_all_roles()).get(local_role, None), 'title', None)
+            objects_string = ''
+            for object in local_roles[local_role]:
+                objects_string += '<a href="%s">%s</a>, ' %(self.url(object),
+                    self.url(object).replace(site_url,''))
+            local_roles_string += '%s: <br />%s <br />' %(role_title,
+                objects_string.rstrip(', '))
+        return local_roles_string
+
+    def getSiteRoles(self, account):
+        site_roles = account.roles
+        site_roles_string = ''
+        for site_role in site_roles:
+            role_title = dict(get_all_roles())[site_role].title
+            site_roles_string += '%s <br />' % role_title
+        return site_roles_string
+
+class AddUserFormPage(KofaAddFormPage):
+    """Add a user account.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.name('add')
+    grok.template('usereditformpage')
+    form_fields = grok.AutoFields(IUserAccount).omit('suspended')
+    label = _('Add officer')
+
+    @action(_('Add officer'), style='primary')
+    def addUser(self, **data):
+        name = data['name']
+        title = data['title']
+        email = data['email']
+        phone = data['phone']
+        description = data['description']
+        #password = data['password']
+        roles = data['roles']
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_secure_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type='danger')
+                return
+        try:
+            self.context.addUser(name, password, title=title, email=email,
+                                 phone=phone, description=description,
+                                 roles=roles)
+            self.flash(_('User account ${a} successfully added.',
+                mapping = {'a': name}))
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.__parent__.logger.info(
+                '%s - added: %s' % (ob_class, name))
+        except KeyError:
+            self.status = self.flash('The userid chosen already exists '
+                                  'in the database.', type='danger')
+            return
+        #self.redirect(self.url(self.context))
+        args = {'identifier':name, 'email':email}
+        self.redirect(self.application_url() + '/changepw?%s' % urlencode(args))
+        return
+
+class UserManageFormPage(KofaEditFormPage):
+    """Manage a user account.
+    """
+    grok.context(IUserAccount)
+    grok.name('manage')
+    grok.template('usereditformpage')
+    grok.require('waeup.manageUsers')
+    form_fields = grok.AutoFields(IUserAccount).omit('name')
+
+    def label(self):
+        return _("Edit officer ${a}", mapping = {'a':self.context.__name__})
+
+    def setUpWidgets(self, ignore_request=False):
+        super(UserManageFormPage,self).setUpWidgets(ignore_request)
+        self.widgets['title'].displayWidth = 30
+        self.widgets['description'].height = 3
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_secure_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type='danger')
+                return
+        changed_fields = self.applyData(self.context, **data)
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        if password:
+            # Now we know that the form has no errors and can set password ...
+            self.context.setPassword(password)
+            changed_fields.append('password')
+        fields_string = ' + '.join(changed_fields)
+        if fields_string:
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.__parent__.logger.info(
+                '%s - %s edited: %s' % (
+                ob_class, self.context.name, fields_string))
+        self.flash(_('User settings have been saved.'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+class ContactUserFormPage(ContactAdminFormPage):
+    grok.name('contactuser')
+    grok.context(IUserAccount)
+    grok.template('contactform')
+    grok.require('waeup.manageUsers')
+    pnav = 0
+    form_fields = grok.AutoFields(IContactForm).select('body')
+
+    def label(self):
+        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
+
+    @action(_('Send message now'), style='primary')
+    def send(self, *args, **data):
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        kofa_utils = getUtility(IKofaUtils)
+        success = kofa_utils.sendContactForm(
+                self.request.principal.title,email,
+                self.context.title,self.context.email,
+                self.request.principal.id,usertype,self.config.name,
+                data['body'],self.config.email_subject)
+        # Success is always True if sendContactForm didn't fail.
+        # TODO: Catch exceptions.
+        if success:
+            self.flash(_('Your message has been sent.'))
+        return
+
+class UserEditFormPage(UserManageFormPage):
+    """Edit a user account by user
+    """
+    grok.name('index')
+    grok.require('waeup.editUser')
+    form_fields = grok.AutoFields(IUserAccount).omit(
+        'name', 'description', 'roles', 'suspended')
+    label = _(u"My Preferences")
+
+    def setUpWidgets(self, ignore_request=False):
+        super(UserManageFormPage,self).setUpWidgets(ignore_request)
+        self.widgets['title'].displayWidth = 30
+
+class MyRolesPage(KofaPage):
+    """Display site roles and local roles assigned to officers.
+    """
+    grok.name('my_roles')
+    grok.require('waeup.editUser')
+    grok.context(IUserAccount)
+    grok.template('myrolespage')
+    label = _(u"My Roles")
+
+    @property
+    def getLocalRoles(self):
+        local_roles = get_user_account(self.request).getLocalRoles()
+        local_roles_userfriendly = {}
+        for local_role in local_roles:
+            role_title = dict(get_all_roles())[local_role].title
+            local_roles_userfriendly[role_title] = local_roles[local_role]
+        return local_roles_userfriendly
+
+    @property
+    def getSiteRoles(self):
+        site_roles = get_user_account(self.request).roles
+        site_roles_userfriendly = []
+        for site_role in site_roles:
+            role_title = dict(get_all_roles())[site_role].title
+            site_roles_userfriendly.append(role_title)
+        return site_roles_userfriendly
+
+class LecturerLandingPage(KofaPage):
+    """Display courses when lecturer logs in.
+    """
+    grok.name('my_courses')
+    grok.require('waeup.Authenticated')
+    grok.context(IUniversity)
+    grok.template('mycourses')
+    label =_('My Courses')
+
+    @property
+    def academic_session(self):
+        current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if current_academic_session:
+            session = academic_sessions_vocab.getTerm(
+                current_academic_session).title
+            return session
+        return _('no academic session set')
+
+    @property
+    def getCourses(self):
+        courses = []
+        local_roles = get_user_account(self.request).getLocalRoles()
+        for item in local_roles.get('waeup.local.Lecturer', []):
+            if isinstance(item, Course):
+                courses.append(item)
+            elif isinstance(item, Department):
+                courses += [i for i in item.courses.values()]
+        return courses
+
+class TranscriptOfficerLandingPage(KofaPage):
+    """Display students, who requested a transcript, when
+    transcript officer logs in.
+    """
+    grok.name('my_transcript_requests')
+    grok.require('waeup.Authenticated')
+    grok.context(IUniversity)
+    grok.template('mytranscriptrequests')
+    label =_('My ToDoList')
+
+    @property
+    def getStudents(self):
+        students = ([],[])
+        cat = queryUtility(ICatalog, name='students_catalog')
+        local_roles = get_user_account(self.request).getLocalRoles()
+        for item in local_roles.get('waeup.local.TranscriptOfficer', []):
+            if isinstance(item, Faculty):
+                faccode = item.code
+                results1 = cat.searchResults(
+                    state=(TRANSREQ, TRANSREQ), faccode=(faccode, faccode))
+                for result in results1:
+                    students[0].append(StudentQueryResultItem(result, self))
+                results2 = cat.searchResults(
+                    state=(TRANSVAL, TRANSVAL), faccode=(faccode, faccode))
+                for result in results2:
+                    students[1].append(StudentQueryResultItem(result, self))
+        return students
+
+class TranscriptSigneeLandingPage(KofaPage):
+    """Display students with validated transcript requests, when
+    transcript signee logs in.
+    """
+    grok.name('my_transcript_signing_requests')
+    grok.require('waeup.Authenticated')
+    grok.context(IUniversity)
+    grok.template('mytranscriptrequests')
+    label =_('My ToDoList')
+
+    @property
+    def getStudents(self):
+        students = ([],[])
+        cat = queryUtility(ICatalog, name='students_catalog')
+        local_roles = get_user_account(self.request).getLocalRoles()
+        for item in local_roles.get('waeup.local.TranscriptSignee', []):
+            if isinstance(item, Faculty):
+                faccode = item.code
+                results = cat.searchResults(
+                    state=(TRANSVAL, TRANSVAL), faccode=(faccode, faccode))
+                for result in results:
+                    students[1].append(StudentQueryResultItem(result, self))
+        return students
+
+#
+# Search pages...
+#
+
+class SearchPage(KofaPage):
+    """General search page for the academic section.
+    """
+    grok.context(IFacultiesContainer)
+    grok.name('search')
+    grok.template('searchpage')
+    grok.require('waeup.viewAcademics')
+    label = _(u"Search Academic Section")
+    pnav = 1
+    search_button = _(u'Search')
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        self.query = ''
+        if not 'query' in form:
+            return
+        query = form['query']
+        self.query = query
+        self.hitlist = search(query=self.query, view=self)
+        if not self.hitlist:
+            self.flash(_('No object found.'), type="warning")
+        return
+
+#
+# Configuration pages...
+#
+
+class ConfigurationContainerManageFormPage(KofaEditFormPage):
+    """Manage page of the configuration container. We always use the
+    manage page in the UI not the view page, thus we use the index name here.
+    """
+    grok.require('waeup.managePortalConfiguration')
+    grok.name('index')
+    grok.context(IConfigurationContainer)
+    grok.template('configurationmanagepage')
+    pnav = 0
+    label = _(u'Edit portal configuration')
+    taboneactions = [_('Save'), _('Update plugins'), _('Purge mandates')]
+    tabtwoactions = [
+        _('Add session configuration'),
+        _('Remove selected')]
+    # We don't need to edit next matric integers in the base package.
+    form_fields = grok.AutoFields(IConfigurationContainer).omit(
+        'frontpage_dict',
+        'next_matric_integer',
+        'next_matric_integer_2',
+        'next_matric_integer_3',
+        'next_matric_integer_4',
+        )
+    form_fields['maintmode_enabled_by'].for_display = True
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        frontpage = getattr(self.context, 'frontpage', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.frontpage_dict = html2dict(frontpage, portal_language)
+        return
+
+    @action(_('Add session configuration'), validator=NullValidator,
+            style='primary')
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    def getSessionConfigurations(self):
+        """Get a list of all stored session configuration objects.
+        """
+        for key, val in self.context.items():
+            url = self.url(val)
+            session_string = val.getSessionString()
+            title = _('Session ${a} Configuration',
+                      mapping = {'a':session_string})
+            yield(dict(url=url, name=key, title=title))
+
+    @jsaction(_('Remove selected'))
+    def delSessonConfigurations(self, **data):
+        delSubobjects(self, redirect='@@index', tab='2')
+        return
+
+    @action(_('Update plugins'),
+              tooltip=_('For experts only!'),
+              warning=_('Plugins may only be updated after software upgrades. '
+                        'Are you really sure?'),
+              validator=NullValidator)
+    def updatePlugins(self, **data):
+        grok.getSite().updatePlugins()
+        self.flash(_('Plugins were updated. See log file for details.'))
+        return
+
+    @action(_('Purge mandates'),
+              tooltip=_('For experts only!'),
+              validator=NullValidator)
+    def purgeMandates(self, **data):
+        num = grok.getSite()['mandates'].removeExpired()
+        self.flash(_('${a} mandate(s) were purged.', mapping = {'a': num}))
+        return
+
+class SessionConfigurationAddFormPage(KofaAddFormPage):
+    """Add a session configuration object to configuration container.
+    """
+    grok.context(IConfigurationContainer)
+    grok.name('add')
+    grok.require('waeup.managePortalConfiguration')
+    label = _('Add session configuration')
+    form_fields = grok.AutoFields(ISessionConfigurationAdd)
+    pnav = 0
+
+    @action(_('Add Session Configuration'), style='primary')
+    def addSessionConfiguration(self, **data):
+        sessionconfiguration = createObject(u'waeup.SessionConfiguration')
+        self.applyData(sessionconfiguration, **data)
+        try:
+            self.context.addSessionConfiguration(sessionconfiguration)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.__parent__.logger.info(
+                '%s - added: %s' % (
+                ob_class, sessionconfiguration.academic_session))
+        except KeyError:
+            self.flash(_('The session chosen already exists.'), type='danger')
+            return
+        self.redirect(self.url(self.context, '@@index')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self):
+        self.redirect(self.url(self.context, '@@index')+'#tab2')
+        return
+
+class SessionConfigurationManageFormPage(KofaEditFormPage):
+    """Manage session configuration object.
+    """
+    grok.context(ISessionConfiguration)
+    grok.name('index')
+    grok.require('waeup.managePortalConfiguration')
+    form_fields = grok.AutoFields(ISessionConfiguration)
+    pnav = 0
+
+    @property
+    def label(self):
+        session_string = self.context.getSessionString()
+        return _('Edit academic session ${a} configuration',
+            mapping = {'a':session_string})
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self):
+        self.redirect(self.url(self.context.__parent__, '@@index')+'#tab2')
+        return
+
+#
+# Datacenter pages...
+#
+
+class DatacenterPage(KofaEditFormPage):
+    grok.context(IDataCenter)
+    grok.name('index')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Data Center')
+    pnav = 0
+    doclink = DOCLINK + '/datacenter.html'
+
+    @jsaction(_('Remove selected'))
+    def delFiles(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No item selected.'), type='danger')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            fullpath = os.path.join(self.context.storage, id)
+            try:
+                os.remove(fullpath)
+                deleted.append(id)
+            except OSError:
+                self.flash(_('OSError: The file could not be deleted.'),
+                           type='danger')
+                return
+        if len(deleted):
+            self.flash(_('Successfully deleted: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
+        return
+
+class DatacenterFinishedPage(KofaEditFormPage):
+    grok.context(IDataCenter)
+    grok.name('processed')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Processed Files')
+    pnav = 0
+    cancel_button =_('Back to Data Center')
+
+    def update(self, CANCEL=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        return super(DatacenterFinishedPage, self).update()
+
+class DatacenterUploadPage(KofaPage):
+    grok.context(IDataCenter)
+    grok.name('upload')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Upload portal data as CSV file')
+    pnav = 0
+    max_files = 20
+    upload_button =_(u'Upload')
+    cancel_button =_(u'Back to Data Center')
+    doclink = DOCLINK + '/datacenter/import_stages.html#stage-1-file-upload'
+
+    def getPreviewHeader(self):
+        """Get the header fields of uploaded CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def _notifyImportManagers(self, filename,
+        normalized_filename, importer, import_mode):
+        """Send email to Import Managers
+        """
+        # Get information about file
+        self.fullpath = os.path.join(self.context.storage, normalized_filename)
+        uploadfile = DataCenterFile(self.fullpath)
+        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        table = getPreviewTable(self, 3)
+        mail_table = ''
+        for line in table:
+            header = line[0]
+            data = str(line[1:]).strip('[').strip(']')
+            mail_table += '%s: %s ...\n' % (line[0], data)
+        # Collect all recipient addresses
+        kofa_utils = getUtility(IKofaUtils)
+        import_managers = get_users_with_role(
+            'waeup.ImportManager', grok.getSite())
+        rcpt_addrs = ','.join(
+            [user['user_email'] for user in import_managers if
+                user['user_email'] is not None])
+        if rcpt_addrs:
+            config = grok.getSite()['configuration']
+            fullname = self.request.principal.title
+            try:
+                email = self.request.principal.email
+            except AttributeError:
+                email = config.email_admin
+            username = self.request.principal.id
+            usertype = getattr(self.request.principal,
+                               'user_type', 'system').title()
+            rcpt_name = _('Import Manager')
+            subject = translate(
+                      _('${a}: ${b} uploaded',
+                      mapping = {'a':config.acronym, 'b':filename}),
+                      'waeup.kofa',
+                      target_language=kofa_utils.PORTAL_LANGUAGE)
+            text = _("""File: ${a}
+Importer: ${b}
+Import Mode: ${c}
+Datasets: ${d}
+
+${e}
+
+Comment by Import Manager:""", mapping = {'a':normalized_filename,
+                'b':importer,
+                'c':import_mode,
+                'd':uploadfile.lines - 1,
+                'e':mail_table})
+            success = kofa_utils.sendContactForm(
+                    fullname,email,
+                    rcpt_name,rcpt_addrs,
+                    username,usertype,config.name,
+                    text,subject)
+            if success:
+                self.flash(
+                    _('All import managers have been notified by email.'))
+            else:
+                self.flash(_('An smtp server error occurred.'), type='danger')
+            return
+
+    def update(self, uploadfile=None, import_mode=None,
+               importer=None, CANCEL=None, SUBMIT=None):
+        number_of_pendings = len(self.context.getPendingFiles())
+        if number_of_pendings > self.max_files:
+            self.flash(
+                _('Maximum number of files in the data center exceeded.'),
+                  type='danger')
+            self.redirect(self.url(self.context))
+            return
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not uploadfile:
+            return
+        try:
+            filename = uploadfile.filename
+            #if 'pending' in filename:
+            #    self.flash(_("You can't re-upload pending data files."), type='danger')
+            #    return
+            if not filename.endswith('.csv'):
+                self.flash(_("Only csv files are allowed."), type='danger')
+                return
+            normalized_filename = self.getNormalizedFileName(filename)
+            finished_file = os.path.join(
+                self.context.storage, 'finished', normalized_filename)
+            unfinished_file = os.path.join(
+                self.context.storage, 'unfinished', normalized_filename)
+            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
+                self.flash(_("File with same name was uploaded earlier."),
+                           type='danger')
+                return
+            target = os.path.join(self.context.storage, normalized_filename)
+            filecontent = uploadfile.read()
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            logger = self.context.logger
+
+            # Forbid certain characters in import files.
+            invalid_line = check_csv_charset(filecontent.splitlines())
+            if invalid_line:
+                if invalid_line == -1:
+                    self.flash(_(
+                        "The data in your file contain trailing whitespaces. "
+                        "Please replace."), type='danger')
+                else:
+                    self.flash(_(
+                        "Your file contains forbidden characters or "
+                        "has invalid CSV format. "
+                        "First problematic line detected: line %s. "
+                        "Please replace." % invalid_line), type='danger')
+                logger.info('%s - invalid file uploaded: %s' %
+                            (ob_class, target))
+                return
+
+            open(target, 'wb').write(filecontent)
+            os.chmod(target, 0664)
+            logger.info('%s - uploaded: %s' % (ob_class, target))
+            self._notifyImportManagers(filename,
+                normalized_filename, importer, import_mode)
+
+        except IOError:
+            self.flash('Error while uploading file. Please retry.', type='danger')
+            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
+            return
+        self.redirect(self.url(self.context))
+
+    def getNormalizedFileName(self, filename):
+        """Build sane filename.
+
+        An uploaded file foo.csv will be stored as foo_USERNAME.csv
+        where username is the principal id of the currently logged in
+        user.
+
+        Spaces in filename are replaced by underscore.
+        Pending data filenames remain unchanged.
+        """
+        if filename.endswith('.pending.csv'):
+            return filename
+        username = self.request.principal.id
+        filename = filename.replace(' ', '_')
+        # Only accept typical filname chars...
+        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
+        base, ext = os.path.splitext(filename)
+        return '%s_%s%s' % (base, filtered_username, ext.lower())
+
+    def getImporters(self):
+        return getImporters(self.context)
+
+class FileDownloadView(UtilityView, grok.View):
+    grok.context(IDataCenter)
+    grok.name('download')
+    grok.require('waeup.manageDataCenter')
+
+    def update(self, filename=None):
+        self.filename = self.request.form['filename']
+        return
+
+    def render(self):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - downloaded: %s' % (ob_class, self.filename))
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' %
+            self.filename.replace('finished/',''))
+        fullpath = os.path.join(self.context.storage, self.filename)
+        return open(fullpath, 'rb').read()
+
+class DatacenterImportStep1(KofaPage):
+    """Manual import step 1: choose file
+    """
+    grok.context(IDataCenter)
+    grok.name('import1')
+    grok.template('datacenterimport1page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Back to Data Center')
+    doclink = DOCLINK + '/datacenter/import_stages.html'
+
+    def getFiles(self):
+        files = self.context.getPendingFiles(sort='date')
+        for file in files:
+            name = file.name
+            if not name.endswith('.csv') and not name.endswith('.pending'):
+                continue
+            yield file
+
+    def update(self, filename=None, select=None, cancel=None):
+        if not grok.getSite()['configuration'].maintmode_enabled_by:
+            self.flash(
+                _('Portal must be in maintenance mode for data import.'),
+                type='warning')
+        if cancel is not None:
+            self.redirect(self.url(self.context))
+            return
+        if select is not None:
+            # A filename was selected
+            session = ISession(self.request)['waeup.kofa']
+            session['import_filename'] = select
+            self.redirect(self.url(self.context, '@@import2'))
+        return
+
+class DatacenterImportStep2(KofaPage):
+    """Manual import step 2: choose processor
+    """
+    grok.context(IDataCenter)
+    grok.name('import2')
+    grok.template('datacenterimport2page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Cancel')
+    back_button =_(u'Back to step 1')
+    proceed_button =_(u'Proceed to step 3')
+    doclink = DOCLINK + '/datacenter/import_stages.html'
+
+    filename = None
+    mode = 'create'
+    importer = None
+    mode_locked = False
+
+    def getPreviewHeader(self):
+        """Get the header fields of attached CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def getPreviewTable(self):
+        return getPreviewTable(self, 3)
+
+    def getImporters(self):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        importers = sorted(
+            [dict(title=x.name, name=x.util_name) for x in importers])
+        return importers
+
+    def getModeFromFilename(self, filename):
+        """Lookup filename or path and return included mode name or None.
+        """
+        if not filename.endswith('.pending.csv'):
+            return None
+        base = os.path.basename(filename)
+        parts = base.rsplit('.', 3)
+        if len(parts) != 4:
+            return None
+        if parts[1] not in ['create', 'update', 'remove']:
+            return None
+        return parts[1]
+
+    def getWarnings(self):
+        import sys
+        result = []
+        try:
+            headerfields = self.getPreviewHeader()
+            headerfields_clean = list(set(headerfields))
+            if len(headerfields) > len(headerfields_clean):
+                result.append(
+                    _("Double headers: each column name may only appear once. "))
+        except:
+            fatal = '%s' % sys.exc_info()[1]
+            result.append(fatal)
+        if result:
+            warnings = ""
+            for line in result:
+                warnings += line + '<br />'
+            warnings += _('Replace imported file!')
+            return warnings
+        return False
+
+    def update(self, mode=None, importer=None,
+               back1=None, cancel=None, proceed=None):
+        session = ISession(self.request)['waeup.kofa']
+        self.filename = session.get('import_filename', None)
+
+        if self.filename is None or back1 is not None:
+            self.redirect(self.url(self.context, '@@import1'))
+            return
+        if cancel is not None:
+            self.flash(_('Import aborted.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        self.mode = mode or session.get('import_mode', self.mode)
+        filename_mode = self.getModeFromFilename(self.filename)
+        if filename_mode is not None:
+            self.mode = filename_mode
+            self.mode_locked = True
+        self.importer = importer or session.get('import_importer', None)
+        session['import_importer'] = self.importer
+        if self.importer and 'update' in self.importer:
+            if self.mode != 'update':
+                self.flash(_('Update mode only!'), type='warning')
+                self.mode_locked = True
+                self.mode = 'update'
+                proceed = None
+        session['import_mode'] = self.mode
+        if proceed is not None:
+            self.redirect(self.url(self.context, '@@import3'))
+            return
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        warnings = self.getWarnings()
+        if not warnings:
+            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        else:
+            self.reader = ()
+            self.flash(warnings, type='warning')
+
+class DatacenterImportStep3(KofaPage):
+    """Manual import step 3: modify header
+    """
+    grok.context(IDataCenter)
+    grok.name('import3')
+    grok.template('datacenterimport3page')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Cancel')
+    reset_button =_(u'Reset')
+    update_button =_(u'Set headerfields')
+    back_button =_(u'Back to step 2')
+    proceed_button =_(u'Perform import')
+    doclink = DOCLINK + '/datacenter/import_stages.html'
+
+    filename = None
+    mode = None
+    importername = None
+
+    @property
+    def nextstep(self):
+        return self.url(self.context, '@@import4')
+
+    def getPreviewHeader(self):
+        """Get the header fields of attached CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def getPreviewTable(self):
+        """Get transposed table with 1 sample record.
+
+        The first column contains the headers.
+        """
+        if not self.reader:
+            return
+        headers = self.getPreviewHeader()
+        num = 0
+        data = []
+        for line in self.reader:
+            if num > 0:
+                break
+            num += 1
+            data.append(line)
+        result = []
+        field_num = 0
+        for name in headers:
+            result_line = []
+            result_line.append(field_num)
+            field_num += 1
+            for d in data:
+                result_line.append(d[name])
+            result.append(result_line)
+        return result
+
+    def getPossibleHeaders(self):
+        """Get the possible headers.
+
+        The headers are described as dicts {value:internal_name,
+        title:displayed_name}
+        """
+        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
+        headers = self.importer.getHeaders()
+        result.extend([dict(title=x, value=x) for x in headers])
+        return result
+
+    def getWarnings(self):
+        import sys
+        result = []
+        try:
+            self.importer.checkHeaders(self.headerfields, mode=self.mode)
+        except:
+            fatal = '%s' % sys.exc_info()[1]
+            result.append(fatal)
+        if result:
+            warnings = ""
+            for line in result:
+                warnings += line + '<br />'
+            warnings += _('Edit headers or replace imported file!')
+            return warnings
+        return False
+
+    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
+        session = ISession(self.request)['waeup.kofa']
+        self.filename = session.get('import_filename', None)
+        self.mode = session.get('import_mode', None)
+        self.importername = session.get('import_importer', None)
+
+        if None in (self.filename, self.mode, self.importername):
+            self.redirect(self.url(self.context, '@@import2'))
+            return
+        if back2 is not None:
+            self.redirect(self.url(self.context ,'@@import2'))
+            return
+        if cancel is not None:
+            self.flash(_('Import aborted.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        self.headerfields = headerfield or self.getPreviewHeader()
+        session['import_headerfields'] = self.headerfields
+
+        if proceed is not None:
+            self.redirect(self.url(self.context, '@@import4'))
+            return
+        self.importer = getUtility(IBatchProcessor, name=self.importername)
+        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+        warnings = self.getWarnings()
+        if warnings:
+            self.flash(warnings, type='warning')
+
+class DatacenterImportStep4(KofaPage):
+    """Manual import step 4: do actual import
+    """
+    grok.context(IDataCenter)
+    grok.name('import4')
+    grok.template('datacenterimport4page')
+    grok.require('waeup.importData')
+    label = _(u'Process CSV file')
+    pnav = 0
+    back_button =_(u'Process next')
+    doclink = DOCLINK + '/datacenter/import_stages.html'
+
+    filename = None
+    mode = None
+    importername = None
+    headerfields = None
+    warnnum = None
+
+    def update(self, back=None, finish=None, showlog=None):
+        if finish is not None:
+            self.redirect(self.url(self.context, '@@import1'))
+            return
+        if not grok.getSite()['configuration'].maintmode_enabled_by and \
+            not self.request.principal.id == 'admin':
+            self.flash(
+                _('File has not been imported. '
+                  'Portal must be in maintenance mode for data import.'),
+                type='danger')
+            self.redirect(self.url(self.context))
+            return
+        session = ISession(self.request)['waeup.kofa']
+        self.filename = session.get('import_filename', None)
+        self.mode = session.get('import_mode', None)
+        self.importername = session.get('import_importer', None)
+        # If the import file contains only one column
+        # the import_headerfields attribute is a string.
+        ihf = session.get('import_headerfields', None)
+        if not isinstance(ihf, list):
+            self.headerfields = ihf.split()
+        else:
+            self.headerfields = ihf
+
+        if None in (self.filename, self.mode, self.importername,
+                    self.headerfields):
+            self.redirect(self.url(self.context, '@@import3'))
+            return
+
+        if showlog is not None:
+            logfilename = "datacenter.log"
+            session['logname'] = logfilename
+            self.redirect(self.url(self.context, '@@show'))
+            return
+
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        self.importer = getUtility(IBatchProcessor, name=self.importername)
+
+        # Perform batch processing...
+        # XXX: This might be better placed in datacenter module.
+        (linenum, self.warn_num,
+         fin_path, pending_path) = self.importer.doImport(
+            self.fullpath, self.headerfields, self.mode,
+            self.request.principal.id, logger=self.context.logger)
+        # Put result files in desired locations...
+        self.context.distProcessedFiles(
+            self.warn_num == 0, self.fullpath, fin_path, pending_path,
+            self.mode)
+
+        # Disable maintenance mode if set.
+        if grok.getSite()['configuration'].maintmode_enabled_by and \
+            not self.request.principal.id == 'admin':
+            grok.getSite()['configuration'].maintmode_enabled_by = None
+            self.flash(_('Maintenance mode disabled.'))
+
+        if self.warn_num:
+            self.flash(_('Processing of ${a} rows failed.',
+                mapping = {'a':self.warn_num}), type='warning')
+        self.flash(_('Successfully processed ${a} rows.',
+            mapping = {'a':linenum - self.warn_num}))
+
+class DatacenterLogsOverview(KofaPage):
+    grok.context(IDataCenter)
+    grok.name('logs')
+    grok.template('datacenterlogspage')
+    grok.require('waeup.manageDataCenter')
+    label = _(u'Show logfiles')
+    pnav = 0
+    back_button = _(u'Back to Data Center')
+    show_button = _(u'Show')
+    doclink = DOCLINK + '/datacenter/logging.html'
+
+    def update(self, back=None):
+        if back is not None:
+            self.redirect(self.url(self.context))
+            return
+        self.files = self.context.getLogFiles()
+
+class DatacenterLogsFileview(KofaPage):
+    grok.context(IDataCenter)
+    grok.name('show')
+    grok.template('datacenterlogsshowfilepage')
+    grok.require('waeup.manageDataCenter')
+    title = _(u'Data Center')
+    pnav = 0
+    search_button = _('Search')
+    back_button = _('Back to Data Center')
+    placeholder = _('Enter a regular expression here...')
+
+    def doclink(self):
+        return DOCLINK + (
+            '/datacenter/logging.html#%s' % self.filename.replace('.', '-'))
+
+    def label(self):
+        return "Logfile %s" % self.filename
+
+    def update(self, back=None, query=None, logname=None):
+        if os.name != 'posix':
+            self.flash(
+                _('Log files can only be searched ' +
+                  'on Unix-based operating systems.'), type='danger')
+            self.redirect(self.url(self.context, '@@logs'))
+            return
+        if back is not None or logname is None:
+            self.redirect(self.url(self.context, '@@logs'))
+            return
+        self.filename = logname
+        self.query = query
+        if not query:
+            return
+        try:
+            self.result = cgi.escape(
+                ''.join(self.context.queryLogfiles(logname, query)))
+        except ValueError:
+            self.flash(_('Invalid search expression.'), type='danger')
+            return
+        if not self.result:
+            self.flash(_('No search results found.'), type='warning')
+        return
+
+class DatacenterSettings(KofaPage):
+    grok.context(IDataCenter)
+    grok.name('manage')
+    grok.template('datacentermanagepage')
+    grok.require('waeup.managePortal')
+    label = _('Edit data center settings')
+    pnav = 0
+    save_button =_(u'Save')
+    reset_button =_(u'Reset')
+    cancel_button =_(u'Back to Data Center')
+
+    def update(self, newpath=None, move=False, overwrite=False,
+               save=None, cancel=None):
+        if self.request.principal.id not in (
+            'admin', 'zope.manager', 'zope.mgr'):
+            self.flash(_('You are not permitted to edit datacenter settings.'),
+                       type='warning')
+            self.redirect(self.url(self.context))
+            return
+        if move:
+            move = True
+        if overwrite:
+            overwrite = True
+        if newpath is None:
+            return
+        if cancel is not None:
+            self.redirect(self.url(self.context))
+            return
+        try:
+            not_copied = self.context.setStoragePath(newpath, move=move)
+            for name in not_copied:
+                self.flash(_('File already existed (not copied): ${a}',
+                    mapping = {'a':name}), type='danger')
+        except:
+            self.flash(_('Given storage path cannot be used. ${a}',
+                        mapping = {'a':sys.exc_info()[1]}), type='danger')
+            return
+        if newpath:
+            self.flash(_('New storage path succefully set.'))
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - storage path set: %s' % (ob_class, newpath))
+            self.redirect(self.url(self.context))
+        return
+
+class ExportCSVPage(KofaPage):
+    grok.context(IDataCenter)
+    grok.name('export')
+    grok.template('datacenterexportpage')
+    grok.require('waeup.exportData')
+    label = _('Download portal data as CSV file')
+    pnav = 0
+    export_button = _(u'Create CSV file')
+    cancel_button =_(u'Back to Data Center')
+    doclink = DOCLINK + '/datacenter/export.html'
+
+    def getExporters(self):
+        utils = getUtilitiesFor(ICSVExporter)
+        STUDENT_EXPORTER_NAMES = getUtility(
+            IStudentsUtils).STUDENT_EXPORTER_NAMES
+        title_name_tuples = [
+            (util.title, name) for name, util in utils
+            if not name in STUDENT_EXPORTER_NAMES]
+        # The exporter for access codes requires a special permission.
+        if not checkPermission('waeup.manageACBatches', self.context):
+            title_name_tuples.remove((u'Access Codes', u'accesscodes'))
+        return sorted(title_name_tuples)
+
+    def update(self, CREATE=None, DISCARD=None, PURGE=None, exporter=None,
+               job_id=None, CANCEL=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if CREATE:
+            ena = exports_not_allowed(self)
+            if ena:
+                self.flash(ena, type='danger')
+                return 
+            job_id = self.context.start_export_job(
+                exporter, self.request.principal.id)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
+        if DISCARD and job_id:
+            entry = self.context.entry_from_job_id(job_id)
+            self.context.delete_export_entry(entry)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - discarded: job_id=%s' % (ob_class, job_id))
+            self.flash(_('Discarded export') + ' %s' % job_id)
+        self.entries = doll_up(self, user=None, purge=PURGE)
+        if PURGE:
+            self.flash(_('Exports successfully purged.'))
+        return
+
+class ExportCSVView(grok.View):
+    grok.context(IDataCenter)
+    grok.name('download_export')
+    grok.require('waeup.exportData')
+
+    def render(self, job_id=None):
+        manager = getUtility(IJobManager)
+        job = manager.get(job_id)
+        if job is None:
+            return
+        if hasattr(job.result, 'traceback'):
+            # XXX: Some error happened. Do something more approriate here...
+            return
+        path = job.result
+        if not os.path.exists(path):
+            # XXX: Do something more appropriate here...
+            return
+        result = open(path, 'rb').read()
+        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
+        filename = "%s_%s" % (acronym, os.path.basename(path))
+        filename = filename.replace('.csv', '_%s.csv' % job_id)
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition', 'attachment; filename="%s' % filename)
+        # remove job and running_exports entry from context
+        #self.context.delete_export_entry(
+        #    self.context.entry_from_job_id(job_id))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
+        return result
+
+class SwitchMaintModeView(UtilityView, grok.View):
+    """Import managers must disable authentication for all other users
+    before starting an import. This pages switches maintenance mode
+    on and off.
+    """
+    grok.context(IDataCenter)
+    grok.name('switchmaintmode')
+    grok.require('waeup.importData')
+
+    def update(self):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        if grok.getSite()['configuration'].maintmode_enabled_by:
+            grok.getSite()['configuration'].maintmode_enabled_by = None
+            self.context.logger.info('%s - maintmode disabled' % ob_class)
+            self.flash(_('Maintenance mode disabled.'))
+            self.redirect(self.url(self.context))
+        else:
+            grok.getSite()['configuration'].maintmode_enabled_by = unicode(
+                self.request.principal.id)
+            self.context.logger.info('%s - maintmode enabled' % ob_class)
+            self.flash(_('Maintenance mode enabled.'), type='warning')
+            self.redirect(self.url(self.context, 'import1'))
+        return
+
+    def render(self):
+        return
+
+#
+# FacultiesContainer pages...
+#
+
+class FacultiesContainerPage(KofaPage):
+    """ Index page for faculty containers.
+    """
+    grok.context(IFacultiesContainer)
+    grok.require('waeup.viewAcademics')
+    grok.name('index')
+    label = _('Academic Section')
+    pnav = 1
+    grok.template('facultypage')
+
+class FacultiesContainerManageFormPage(KofaEditFormPage):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFacultiesContainer)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    grok.template('facultiescontainermanagepage')
+    pnav = 1
+    taboneactions = [_('Add faculty'), _('Remove selected'),_('Cancel')]
+    subunits = _('Faculties')
+    doclink = DOCLINK + '/academics.html'
+
+    @property
+    def label(self):
+        return _('Manage academic section')
+
+    @jsaction(_('Remove selected'))
+    def delFaculties(self, **data):
+        if not checkPermission('waeup.managePortal', self.context):
+            self.flash(_('You are not allowed to remove entire faculties.'),
+                       type='warning')
+            return
+        delSubobjects(self, redirect='@@manage', tab='1')
+        return
+
+    @action(_('Add faculty'), validator=NullValidator)
+    def addFaculty(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class FacultyAddFormPage(KofaAddFormPage):
+    """ Page form to add a new faculty to a faculty container.
+    """
+    grok.context(IFacultiesContainer)
+    grok.require('waeup.manageAcademics')
+    grok.name('add')
+    label = _('Add faculty')
+    form_fields = grok.AutoFields(IFaculty)
+    pnav = 1
+    doclink = DOCLINK + '/academics.html#faculties'
+
+    @action(_('Add faculty'), style='primary')
+    def addFaculty(self, **data):
+        faculty = createObject(u'waeup.Faculty')
+        self.applyData(faculty, **data)
+        try:
+            self.context.addFaculty(faculty)
+        except KeyError:
+            self.flash(_('The faculty code chosen already exists.'),
+                       type='danger')
+            return
+        self.flash(
+            _("Faculty ${a} added.", mapping = {'a':data['code']}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.logger.info(
+            '%s - added: %s' % (ob_class, faculty.code))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab1')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+#
+# Faculty pages
+#
+class FacultyPage(KofaPage):
+    """Index page of faculties.
+    """
+    grok.context(IFaculty)
+    grok.require('waeup.viewAcademics')
+    grok.name('index')
+    pnav = 1
+
+    @property
+    def label(self):
+        return _('Departments')
+
+class FacultyManageFormPage(KofaEditFormPage,
+                            LocalRoleAssignmentUtilityView):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFaculty)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    grok.template('facultymanagepage')
+    pnav = 1
+    subunits = _('Departments')
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add department'), _('Remove selected'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+    doclink = DOCLINK + '/academics.html#faculties'
+
+    form_fields = grok.AutoFields(IFaculty).omit('code')
+
+    @property
+    def label(self):
+        return _('Manage faculty')
+
+    @jsaction(_('Remove selected'))
+    def delDepartments(self, **data):
+        if not checkPermission('waeup.managePortal', self.context):
+            self.flash(_('You are not allowed to remove entire departments.'),
+                       type='danger')
+            return
+        delSubobjects(self, redirect='@@manage', tab='2')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add department'), validator=NullValidator)
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class FindStudentsPage(KofaPage):
+    """Search students in faculty.
+    """
+    grok.context(IFaculty)
+    grok.name('find_students')
+    grok.require('waeup.showStudents')
+    grok.template('findstudentspage')
+    search_button = _('Find student(s)')
+    pnav = 1
+
+    @property
+    def label(self):
+        return _('Find students in ') + ('%s' % self.context.longtitle)
+
+    def _find_students(self,query=None, searchtype=None, view=None):
+        hitlist = []
+        if searchtype in ('fullname',):
+            results = Query().searchResults(
+                Text(('students_catalog', searchtype), query) &
+                Eq(('students_catalog', 'faccode'), self.context.code)
+                )
+        else:
+            results = Query().searchResults(
+                Eq(('students_catalog', searchtype), query) &
+                Eq(('students_catalog', 'faccode'), self.context.code)
+                )
+        for result in results:
+            hitlist.append(StudentQueryResultItem(result, view=view))
+        return hitlist
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        if 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type='warning')
+            return
+        self.hitlist = self._find_students(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash(_('No student found.'), type='warning')
+        return
+
+class DepartmentAddFormPage(KofaAddFormPage):
+    """Add a department to a faculty.
+    """
+    grok.context(IFaculty)
+    grok.name('add')
+    grok.require('waeup.manageAcademics')
+    label = _('Add department')
+    form_fields = grok.AutoFields(IDepartment)
+    pnav = 1
+    doclink = DOCLINK + '/academics.html#departments'
+
+    @action(_('Add department'), style='primary')
+    def addDepartment(self, **data):
+        department = createObject(u'waeup.Department')
+        self.applyData(department, **data)
+        try:
+            self.context.addDepartment(department)
+        except KeyError:
+            self.flash(_('The code chosen already exists in this faculty.'),
+                       type='danger')
+            return
+        self.flash(
+            _("Department ${a} added.", mapping = {'a':data['code']}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.__parent__.logger.info(
+            '%s - added: %s/%s' % (ob_class, self.context.code, data['code']))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Department pages
+#
+class DepartmentPage(KofaPage):
+    """Department index page.
+    """
+    grok.context(IDepartment)
+    grok.require('waeup.viewAcademics')
+    grok.name('index')
+    pnav = 1
+    label = _('Courses and Certificates')
+
+    def update(self):
+        super(DepartmentPage, self).update()
+        return
+
+    def getCourses(self):
+        """Get a list of all stored courses.
+        """
+        for key, val in self.context.courses.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+    def getCertificates(self):
+        """Get a list of all stored certificates.
+        """
+        for key, val in self.context.certificates.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+class DepartmentManageFormPage(KofaEditFormPage,
+                               LocalRoleAssignmentUtilityView):
+    """Manage the basic properties of a `Department` instance.
+    """
+    grok.context(IDepartment)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    pnav = 1
+    grok.template('departmentmanagepage')
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add course'), _('Remove selected courses'),_('Cancel')]
+    tabthreeactions = [_('Add certificate'), _('Remove selected certificates'),
+                       _('Cancel')]
+    tabfouractions1 = [_('Remove selected local roles')]
+    tabfouractions2 = [_('Add local role')]
+    doclink = DOCLINK + '/academics.html#departments'
+
+    form_fields = grok.AutoFields(IDepartment).omit('code')
+
+    @property
+    def label(self):
+        return _('Manage department')
+
+    def getCourses(self):
+        """Get a list of all stored courses.
+        """
+        for key, val in self.context.courses.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+    def getCertificates(self):
+        """Get a list of all stored certificates.
+        """
+        for key, val in self.context.certificates.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @jsaction(_('Remove selected courses'))
+    def delCourses(self, **data):
+        delSubobjects(
+            self, redirect='@@manage', tab='2', subcontainer='courses')
+        return
+
+    @jsaction(_('Remove selected certificates'))
+    def delCertificates(self, **data):
+        if not checkPermission('waeup.managePortal', self.context):
+            self.flash(_('You are not allowed to remove certificates.'),
+                       type='warning')
+            return
+        delSubobjects(
+            self, redirect='@@manage', tab='3', subcontainer='certificates')
+        return
+
+    @action(_('Add course'), validator=NullValidator)
+    def addCourse(self, **data):
+        self.redirect(self.url(self.context, 'addcourse'))
+        return
+
+    @action(_('Add certificate'), validator=NullValidator)
+    def addCertificate(self, **data):
+        self.redirect(self.url(self.context, 'addcertificate'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,4,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,4,**data)
+
+class CourseAddFormPage(KofaAddFormPage):
+    """Add-form to add course to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcourse')
+    grok.require('waeup.manageAcademics')
+    label = _(u'Add course')
+    form_fields = grok.AutoFields(ICourse)
+    pnav = 1
+    doclink = DOCLINK + '/academics.html#courses'
+
+    @action(_('Add course'))
+    def addCourse(self, **data):
+        course = createObject(u'waeup.Course')
+        self.applyData(course, **data)
+        try:
+            self.context.courses.addCourse(course)
+        except DuplicationError, error:
+            # signals duplication error
+            entries = error.entries
+            for entry in entries:
+                # do not use error.msg but render a more detailed message instead
+                # and show links to all certs with same code
+                message = _('A course with same code already exists: ')
+                message += '<a href="%s">%s</a>' % (
+                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
+                self.flash(message, type='warning')
+            self.redirect(self.url(self.context, u'@@addcourse'))
+            return
+        message = _(u'Course ${a} successfully created.', mapping = {'a':course.code})
+        self.flash(message)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.__parent__.__parent__.logger.info(
+            '%s - added: %s' % (ob_class, data['code']))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class CertificateAddFormPage(KofaAddFormPage):
+    """Add-form to add certificate to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcertificate')
+    grok.require('waeup.manageAcademics')
+    label = _(u'Add certificate')
+    form_fields = grok.AutoFields(ICertificate)
+    pnav = 1
+    doclink = DOCLINK + '/academics.html#certificates'
+
+    @action(_('Add certificate'))
+    def addCertificate(self, **data):
+        certificate = createObject(u'waeup.Certificate')
+        self.applyData(certificate, **data)
+        try:
+            self.context.certificates.addCertificate(certificate)
+        except DuplicationError, error:
+            # signals duplication error
+            entries = error.entries
+            for entry in entries:
+                # do not use error.msg but render a more detailed message instead
+                # and show links to all certs with same code
+                message = _('A certificate with same code already exists: ')
+                message += '<a href="%s">%s</a>' % (
+                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
+                self.flash(message, type='warning')
+            self.redirect(self.url(self.context, u'@@addcertificate'))
+            return
+        message = _(u'Certificate ${a} successfully created.', mapping = {'a':certificate.code})
+        self.flash(message)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.__parent__.__parent__.logger.info(
+            '%s - added: %s' % (ob_class, data['code']))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab3')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self): #, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Courses pages
+#
+class CoursePage(KofaDisplayFormPage):
+    """Course index page.
+    """
+    grok.context(ICourse)
+    grok.name('index')
+    grok.require('waeup.viewAcademics')
+    pnav = 1
+    form_fields = grok.AutoFields(ICourse)
+    form_fields[
+        'results_validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+    @property
+    def label(self):
+        return '%s (%s)' % (self.context.title, self.context.code)
+
+class CourseManageFormPage(KofaEditFormPage,
+                           LocalRoleAssignmentUtilityView):
+    """Edit form page for courses.
+    """
+    grok.context(ICourse)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    grok.template('coursemanagepage')
+    label = _(u'Edit course')
+    pnav = 1
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions1 = [_('Remove selected local roles')]
+    tabtwoactions2 = [_('Add local role')]
+    doclink = DOCLINK + '/academics.html#courses'
+
+    form_fields = grok.AutoFields(ICourse).omit('code')
+    form_fields['results_validated_by'].for_display = True
+    form_fields['results_validation_date'].for_display = True
+    #form_fields['results_validation_session'].for_display = True
+    form_fields[
+        'results_validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,2,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,2,**data)
+
+class UpdateCourseTicketsView(UtilityView, grok.View):
+    """Udate course tickets.
+    """
+    grok.context(ICourse)
+    grok.name('update_coursetickets')
+    grok.require('waeup.editScores')
+
+    def update(self):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        grok.getSite().logger.info(
+            '%s - course tickets updated: %s' % (
+                ob_class, self.context.code))
+        number = getUtility(IStudentsUtils).updateCourseTickets(self.context)
+        if number is None:
+            self.flash(_('Current academic session not set.'))
+        elif number is 0:
+            self.flash(_('No course ticket found.'))
+        else:
+            self.flash(
+                _('%s course ticket(s) updated.' % number))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+#
+# Certificate pages
+#
+class CertificatePage(KofaDisplayFormPage):
+    """Index page for certificates.
+    """
+    grok.context(ICertificate)
+    grok.name('index')
+    grok.require('waeup.viewAcademics')
+    pnav = 1
+    form_fields = grok.AutoFields(ICertificate)
+    grok.template('certificatepage')
+
+    @property
+    def label(self):
+        return "%s (%s)" % (self.context.title, self.context.code)
+
+    def update(self):
+        return super(CertificatePage, self).update()
+
+class CertificateManageFormPage(KofaEditFormPage,
+                                LocalRoleAssignmentUtilityView):
+    """Manage the properties of a `Certificate` instance.
+    """
+    grok.context(ICertificate)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    pnav = 1
+    label = _('Edit certificate')
+    doclink = DOCLINK + '/academics.html#certificates'
+
+    form_fields = grok.AutoFields(ICertificate).omit('code')
+
+    pnav = 1
+    grok.template('certificatemanagepage')
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add certificate course'),
+                     _('Remove selected certificate courses'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+
+    @property
+    def label(self):
+        return _('Manage certificate')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @jsaction(_('Remove selected certificate courses'))
+    def delCertificateCourses(self, **data):
+        delSubobjects(self, redirect='@@manage', tab='2')
+        return
+
+    @action(_('Add certificate course'), validator=NullValidator)
+    def addCertificateCourse(self, **data):
+        self.redirect(self.url(self.context, 'addcertificatecourse'))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+
+class CertificateCourseAddFormPage(KofaAddFormPage):
+    """Add-page to add a course ref to a certificate
+    """
+    grok.context(ICertificate)
+    grok.name('addcertificatecourse')
+    grok.require('waeup.manageAcademics')
+    form_fields = grok.AutoFields(ICertificateCourse)
+    pnav = 1
+    label = _('Add certificate course')
+    doclink = DOCLINK + '/academics.html#certificate-courses'
+
+    @action(_('Add certificate course'))
+    def addCertcourse(self, **data):
+        try:
+            self.context.addCertCourse(**data)
+        except KeyError:
+            self.status = self.flash(_('The chosen certificate course is already '
+                                  'part of this certificate.'), type='warning')
+            return
+        self.status = self.flash(
+            _("certificate course ${a}_${b} added.",
+            mapping = {'a': data['course'].code, 'b': data['level']}))
+        code = "%s_%s" % (data['course'].code, data['level'])
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        grok.getSite().logger.info('%s - added: %s' % (ob_class, code))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+#
+# Certificate course pages...
+#
+class CertificateCoursePage(KofaPage):
+    """CertificateCourse index page.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('index')
+    grok.require('waeup.viewAcademics')
+    pnav = 1
+    #form_fields = grok.AutoFields(ICertificateCourse)
+
+    @property
+    def label(self):
+        return '%s' % (self.context.longtitle)
+
+    @property
+    def leveltitle(self):
+        return course_levels.getTerm(self.context.level).title
+
+    @property
+    def course_category_title(self):
+        cc = self.context.course_category
+        if cc:
+            return getUtility(IKofaUtils).COURSE_CATEGORY_DICT[cc]
+        return
+
+class CertificateCourseManageFormPage(KofaEditFormPage):
+    """Manage the basic properties of a `CertificateCourse` instance.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('manage')
+    grok.require('waeup.manageAcademics')
+    form_fields = grok.AutoFields(ICertificateCourse).omit('code')
+    label = _('Edit certificate course')
+    pnav = 1
+    doclink = DOCLINK + '/academics.html#certificate-courses'
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        parent = self.context.__parent__
+        if self.context.level == data['level']:
+            msave(self, **data)
+        else:
+            # The level changed. We have to create a new certcourse as
+            # the key of the entry will change as well...
+            old_level = self.context.level
+            data['course'] = self.context.course
+            try:
+                parent.addCertCourse(**data)
+            except KeyError:
+                self.flash(_('The chosen certificate course is already '
+                             'part of this certificate.'), type='warning')
+                return
+            parent.delCertCourses(data['course'].code, level=old_level)
+            self.flash(_('Form has been saved.'))
+            old_code = "%s_%s" % (data['course'].code, old_level)
+            new_code = "%s_%s" % (data['course'].code, data['level'])
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            grok.getSite().logger.info(
+                '%s - renamed: %s to %s' % (ob_class, old_code, new_code))
+            self.redirect(self.url(parent) + '/%s_%s/@@manage' % (
+                data['course'].code, data['level']))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class ChangePasswordRequestPage(KofaFormPage):
+    """Captcha'd page for all kind of users to request a password change.
+    """
+    grok.context(IUniversity)
+    grok.name('changepw')
+    grok.require('waeup.Anonymous')
+    grok.template('changepw')
+    form_fields = grok.AutoFields(IChangePassword)
+
+    def update(self):
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode. '
+                        'Password request forms are temporarily disabled.'),
+                       type='warning')
+            self.redirect(self.url(self.context))
+            return
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        # Unset default values maybe set by another person who used this form.
+        self.form_fields.get('identifier').field.default = None
+        self.form_fields.get('email').field.default = None
+        self.auto_fill = False
+        # If the form is used with url encoded form parameters.
+        form = self.request.form
+        if form.get('identifier') and form.get('email'):
+            try:
+                self.form_fields.get('identifier').field.default = form.get('identifier')
+                self.form_fields.get('email').field.default = str(form.get('email'))
+            except NotAnEmailAddress:
+                self.flash(_('This is not a valid email address.'),
+                       type='danger')
+                self.redirect(self.url(self.context))
+                return
+            self.auto_fill = True
+        return
+
+    def label(self):
+        if self.auto_fill:
+            return _('Send user a new password')
+        return _('Send me a new password')
+
+    def _searchUser(self, identifier, email):
+        # Search student
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(
+            email=(email,email))
+        for result in results:
+            if result.student_id == identifier or \
+                result.reg_number == identifier or \
+                result.matric_number == identifier:
+                return result
+        # Search applicant
+        cat = queryUtility(ICatalog, name='applicants_catalog')
+        if cat is not None:
+            results = cat.searchResults(
+                email=(email,email))
+            for result in results:
+                if result.applicant_id == identifier or \
+                    result.reg_number == identifier:
+                    return result
+        # Search officer
+        user = grok.getSite()['users'].get(identifier, None)
+        if user is not None and user.email == email:
+            return user
+        return None
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def request(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        # Search student or applicant
+        identifier = data['identifier']
+        email = data['email']
+        user = self._searchUser(identifier, email)
+        if user is None:
+            self.flash(_('No record found.'), type='warning')
+            return
+        # Change password
+        kofa_utils = getUtility(IKofaUtils)
+        password = kofa_utils.genPassword()
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = user
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully requested a password for the')
+        success = kofa_utils.sendCredentials(
+            IUserAccount(user),password,url_info,msg)
+        if success:
+            self.flash(_('An email with your user name and password ' +
+                'has been sent to ${a}.', mapping = {'a':email}))
+        else:
+            self.flash(_('An smtp server error occurred.'), type='danger')
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pdf.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pdf.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/pdf.py	(revision 16045)
@@ -0,0 +1,938 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Reusable components for pdf generation.
+"""
+import grok
+import os
+import pytz
+from cStringIO import StringIO
+from datetime import datetime
+from reportlab.graphics.barcode.qr import QrCodeWidget
+from reportlab.graphics.shapes import Drawing
+from reportlab.lib import colors
+from reportlab.lib.units import cm, inch, mm
+from reportlab.lib.pagesizes import A4, A3, landscape, portrait
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.platypus import (
+    SimpleDocTemplate, Spacer, Paragraph, Image, Table)
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from zope.publisher.browser import TestRequest
+from zope.component import getUtility, queryUtility
+from waeup.kofa.browser.interfaces import IPDFCreator
+from waeup.kofa.utils.helpers import now
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+
+
+#: A reportlab paragraph style for 'normal' output.
+NORMAL_STYLE = getSampleStyleSheet()['Normal']
+
+#: A reportlab paragraph style for 'normal' output.
+HEADING3_STYLE = getSampleStyleSheet()['Heading3']
+
+#: A reportlab paragraph style for headings.
+HEADING_STYLE = ParagraphStyle(
+    name='Heading3',
+    parent=HEADING3_STYLE,
+    fontSize=11,
+    )
+
+#: A reportlab paragraph style for output of 'code'.
+CODE_STYLE = ParagraphStyle(
+    name='Code',
+    parent=NORMAL_STYLE,
+    fontName='Courier',
+    fontSize=10,
+    leading=10,
+    )
+
+#: A reportlab paragraph style for regular form output.
+ENTRY1_STYLE = ParagraphStyle(
+    name='Entry1',
+    parent=NORMAL_STYLE,
+    fontSize=11,
+    leading=10,
+    )
+
+#: A reportlab paragraph style for smaller form output.
+SMALL_PARA_STYLE = ParagraphStyle(
+    name='Small1',
+    parent=NORMAL_STYLE,
+    fontSize=8,
+    )
+
+#: A reportlab paragraph style for headlines or bold text in form output.
+HEADLINE1_STYLE = ParagraphStyle(
+    name='Header1',
+    parent=NORMAL_STYLE,
+    fontName='Helvetica-Bold',
+    fontSize=11,
+    )
+
+#: A reportlab paragraph style for notes output at end of documents.
+NOTE_STYLE = ParagraphStyle(
+    name='Note',
+    parent=NORMAL_STYLE,
+    fontName='Helvetica',
+    fontSize=10,
+    #leading=9,
+    )
+
+#: Base style for signature tables
+SIGNATURE_TABLE_STYLE = [
+    ('VALIGN',(0,-1),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
+    ('BOTTOMPADDING', (0,0), (-1,0), 36),
+    ('TOPPADDING', (0,-1), (-1,-1), 0),
+    ]
+
+
+def format_html(html):
+    """Make HTML code usable for use in reportlab paragraphs.
+
+    Main things fixed here:
+    If html code:
+    - remove newlines (not visible in HTML but visible in PDF)
+    - add <br> tags after <div> (as divs break lines in HTML but not in PDF)
+    - replace <li> tags (as lists are not supported by reportlab)
+    If not html code:
+    - just replace newlines by <br> tags
+    """
+    if u'₦' in html:
+        html = html.replace(u'₦', u'NGN')
+    if '</' in html:
+        html = html.replace('</div>', '</div><br />')
+        html = html.replace('<li>', '- ')
+        html = html.replace('</li>', '<br />')
+        html = html.replace('\n', '')
+    else:
+        html = html.replace('\n', '<br />')
+    return html
+
+def normalize_signature(signature_tuple):
+    """Normalize a signature tuple.
+
+    Returns a tuple ``(<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)`` from
+    input tuple. The following rules apply::
+
+      (pre, sig, post)  --> (pre, sig, post)
+      (pre, sig)        --> (pre, sig, None)
+      (sig)             --> (None, sig, None)
+
+    Also simple strings are accepted as input::
+
+      sig               --> (None, sig, None)
+
+    If input is not a tuple nor a basestring or if the tuple contains
+    an invalid number of elements, ``ValueError`` is raised.
+    """
+    if isinstance(signature_tuple, basestring):
+        return (None, signature_tuple, None)
+    if not isinstance(signature_tuple, tuple):
+        raise ValueError("signature_tuple must be a string or tuple")
+    if len(signature_tuple) < 1 or len(signature_tuple) > 3:
+        raise ValueError("signature_tuple must have 1, 2, or 3 elements")
+    elif len(signature_tuple) == 1:
+        signature_tuple = (None, signature_tuple[0], None)
+    elif len(signature_tuple) == 2:
+        signature_tuple = (signature_tuple[0], signature_tuple[1], None)
+    return signature_tuple
+
+def vert_signature_cell(signature, date_field=True, date_text=_('Date:'),
+                        start_row=0, start_col=0, underline=True):
+    """Generate a table part containing a vertical signature cell.
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    `underline`:
+       boolean indicating that the signature cell should provide a line on
+       top (`True` by default).
+
+    Vertical signature cells look like this::
+
+      +------------+
+      |Pre         |
+      +------------+
+      |Date:       |
+      |            |
+      +------------+
+      | ---------- |
+      | Signature  |
+      +------------+
+      |Post        |
+      +------------+
+    """
+    # split signature parts, replacing None with empty string
+    pre, sig, post = [x or '' for x in signature]
+    style = ()
+    x, y = start_col, start_row+2
+    if underline:
+        style += (('LINEABOVE', (x, y), (x, y), 1, colors.black),)
+    d_text = date_field and date_text or ''
+    data = [[pre], [d_text], [sig], [post]]
+    col_widths = [1.0]
+    return data, style, col_widths
+
+def horiz_signature_cell(signature, date_field=True, date_text=_('Date'),
+                         start_row=0, start_col=0):
+    """Generate a table part containing an horizontal signature cell
+
+    Returns the table data as list of lists and an according style.
+
+    `signature`:
+       a signature tuple containing (<PRE-TEXT, SIGNATURE-TEXT, POST-TEXT>)
+
+    `date_field`:
+       boolean indicating that a 'Date:' text should be rendered into this
+       signature cell (or not).
+
+    `date_text`:
+       the text to be rendered into the signature field as 'Date:' text.
+
+    `start_row`:
+       starting row of the signature cell inside a broader table.
+
+    `start_col`:
+       starting column of the signature cell inside a broader table.
+
+    Horizontal signature cells look like this::
+
+      +------------+---+-----------+
+      |Pre text possibly filling   |
+      |the whole box               |
+      +------------+---+-----------+
+      |            |   |           |
+      |            |   |           |
+      +------------+---+-----------+
+      | ---------- |   | --------- |
+      | Date       |   | Signature |
+      +------------+---+-----------+
+      |Post                        |
+      +------------+---+-----------+
+
+    """
+    pre, sig, post = signature
+    if not date_field:
+        data, style, cols = vert_signature_cell(signature, date_field=False)
+        return data, style, cols
+    style = (
+        # let pre and post text span the whole signature cell
+        ('SPAN', (start_col, start_row), (start_col+2, start_row)),
+        ('SPAN', (start_col, start_row+3), (start_col+2, start_row+3)),
+        )
+    # horizontal cells are buildt from vertical ones chained together
+    cell1 = vert_signature_cell(  # leftmost date col
+        (pre, date_text, post), date_field=False,
+        start_row=start_row, start_col=start_col)
+    cell2 = vert_signature_cell(  # spacer col (between date and sig)
+        ('', '', ''), date_field=False, underline=False,
+        start_row=start_row, start_col=start_col+1)
+    cell3 = vert_signature_cell(  # rightmost signature column
+        ('', sig, ''), date_field=False,
+        start_row=start_row, start_col=start_col+2)
+    data = map(lambda x, y, z: x+y+z, cell1[0], cell2[0], cell3[0])
+    style = style + cell1[1] + cell2[1] + cell3[1]
+    col_widths  = [0.3, 0.03, 0.67] # sums up to 1.0
+    return data, style, col_widths
+
+def signature_row(signatures, start_row=0, horizontal=None, max_per_row=3):
+    data = [[], [], [], []]
+    style = ()
+    signatures = [normalize_signature(sig) for sig in signatures]
+    start_col = 0
+    col_widths = []
+
+    if horizontal is None:
+        horizontal = len(signatures) == 1
+    cell_maker = vert_signature_cell
+    if horizontal:
+        cell_maker = horiz_signature_cell
+    main_cell_height = not horizontal and 36 or 18
+
+    for sig in signatures:
+        sig_data, sig_style, sig_cols = cell_maker(
+            sig, start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, sig_data)
+        style += sig_style
+        col_widths += sig_cols + [None,]
+
+        start_col += 1
+        # add spacer
+        spacer, spacer_style, cols = vert_signature_cell(
+            ('', '', ''), date_field=False, underline=False,
+            start_row=start_row, start_col=start_col)
+        data = map(lambda x, y: x+y, data, spacer)
+        style += spacer_style
+        start_col += 1
+
+    y = start_row
+    sig_row = start_row + 2
+    style = style + (
+        ('TOPPADDING', (0, y+2), (-1, y+2), 0), # signature row
+        ('BOTTOMPADDING', (0, y+1), (-1, y+1), main_cell_height),
+        ('LEFTPADDING', (0, y), (-1, y), 1), # pre row
+        ('LEFTPADDING', (0, y+3), (-1, y+3), 1), # post row
+        )
+
+    if len(signatures) == 1:
+        # pre and post text should span whole table
+        style += (('SPAN', (0, y), (-1, y)),
+                  ('SPAN', (0, y+3), (-1, y+3)),
+                  )
+
+    if data[0] == [''] * len(data[0]):
+        # no pre text: hide pre row by minimizing padding
+        style += (('TOPPADDING', (0,y), (-1, y), -6),
+                  ('BOTTOMPADDING', (0,y), (-1, y), -6),
+                  )
+    if data[-1] == [''] * len(data[0]):
+        # no post text: hide post row by minimizing padding
+        style += (('TOPPADDING', (0,y+3), (-1, y+3), -6),
+                  ('BOTTOMPADDING', (0,y+3), (-1, y+3), -6),
+                  )
+
+    if len(signatures) > 1:
+        data = [x[:-1] for x in data] # strip last spacer
+        col_widths = col_widths[:-1]
+    return data, style, col_widths
+
+def sig_table(signatures, lang='en', max_per_row=3, horizontal=None,
+              single_table=False, start_row=0, landscape=False):
+    if landscape:
+        space_width = 2.4  # width in cm of space between signatures
+        table_width = 24.0 # supposed width of signature table in cms
+    else:
+        space_width = 0.4  # width in cm of space between signatures
+        table_width = 16.0 # supposed width of signature table in cms
+    # width of signature cells in cm...
+    sig_num = len(signatures)
+    sig_col_width = (table_width - ((sig_num - 1) * space_width)) / sig_num
+    if sig_num == 1:
+        sig_col_width = 0.8 * table_width         # signature cell part
+        space_width = table_width - sig_col_width # spacer part on the right
+
+    if sig_num > max_per_row:
+        if horizontal is None:
+            horizontal = max_per_row == 1
+        sigs_by_row = [signatures[x:x+max_per_row] for x in range(
+            0, sig_num, max_per_row)]
+        result = []
+        curr_row = 0
+        for num, row_sigs in enumerate(sigs_by_row):
+            curr_row = 0
+            if single_table:
+                curr_row = num * 4
+            result.append(
+                sig_table(row_sigs, lang=lang, max_per_row=max_per_row,
+                          horizontal=horizontal, start_row=curr_row,
+                          landscape=landscape)[0],
+                          )
+        missing_num = len(result[-2][0][0]) - len(result[-1][0][0])
+        if missing_num:
+            # last row contained less cells, fix it...
+            result[-1] = ([x + [''] * missing_num for x in result[-1][0]],
+                          result[-1][1], result[-2][2])
+        return result
+
+    data, style, cols = signature_row(signatures, horizontal=horizontal,
+                                      start_row=start_row)
+    style += (('VALIGN', (0,0), (-1,-1), 'TOP'),)
+
+    # compute col widths...
+    col_widths = []
+    for col in cols:
+        if col is not None:
+            col = col * sig_col_width * cm
+        else:
+            col = space_width * cm
+        col_widths.append(col)
+
+    # replace strings by paragraphs and translate all contents
+    for rnum, row in enumerate(data):
+        for cnum, cell in enumerate(row):
+            if cell:
+                content = translate(cell, lang)
+                data[rnum][cnum] = Paragraph(content, NORMAL_STYLE)
+    return [(data, style, col_widths),]
+
+def get_sig_tables(signatures, lang='en', max_per_row=3, horizontal=None,
+                   single_table=False, landscape=False):
+    rows = sig_table(signatures, lang=lang, max_per_row=max_per_row,
+                     horizontal=horizontal, single_table=single_table,
+                     landscape=landscape)
+    if single_table:
+        result_data = []
+        result_style = ()
+        for row in rows:
+            data, style, col_widths = row
+            result_data += data
+            result_style += style
+        return [(result_data, result_style, col_widths),]
+    return rows
+
+def get_signature_tables(signatures, lang='en', max_per_row=3,
+                         horizontal=None, single_table=False,
+                         landscape=False):
+    """Get a list of reportlab flowables representing signature fields.
+
+    `signatures` is a list of signatures. Each signature can be a
+    simple string or a tuple of format::
+
+      (<PRE-TEXT>, <SIGNATURE>, <POST-TEXT>)
+
+    where ``<PRE-TEXT>`` and ``<POST-TEXT>`` are texts that should
+    appear on top (PRE) or below (POST) the signature cell. Both
+    formats, string and tuple, can be mixed. A single signature would
+    be given as ``[('Pre-Text', 'Signature', 'Post-Text'),]`` or
+    simply as ``['Signature']`` if not pre or post-text is wanted.
+
+    All texts (pre, sig, post) are rendered as paragraphs, so you can
+    pass in also longer texts with basic HTML formatting like ``<b>``,
+    ``<i>``, ``<br />``, etc.
+
+    ``lang`` sets the language to use in I18n context. All texts are
+    translated to the given language (``en`` by default) if a
+    translation is available.
+
+    ``max_per_row`` gives the maximum number of signatures to put into
+    a single row. The default is 3. If more signatures are passed in,
+    these signatures are put into a new row. So, for example by
+    default 8 signatures would be spread over 3 rows.
+
+    ``horizontal`` tells how the single signature cells should be
+    rendered: horizontal or vertical. While horizontal cells render
+    date and signature fields side by side, in vertical cells date is
+    rendered on top of the signature.
+
+    This parameter accepts *three* different values: ``True``,
+    ``False``, or ``None``. While with ``True`` each cell is rendered
+    in horizontal mode, ``False`` will create only vertical cells.
+
+    The ``None`` value (set by default) is different: if set, the mode
+    will be dependent on the number of signatures per row. If a row
+    contains exactly one signature (because only one sig was passed
+    in, or because ``max_per_row`` was set to ``1``), then this
+    signature is rendered in horizontal mode. Otherwise (with more
+    than one sig per row) each cell is rendered in vertical mode. This
+    pseudo-smart behaviour can be switched off by setting
+    ``horizontal`` explicitly to ``True`` or ``False``.
+
+    ``single_table`` is a boolean defaulting to ``False``. By default
+    we return the rows of a signature table in several tables, one of
+    each row. This makes it easier for reportlab to perform pagebreaks
+    in case the page is already full, without wasting space. If the
+    parameter is set to ``True``, then always a list with exactly one
+    table is returned, which will contain all rows in one table.
+
+    Generally, if a row contains only one signature, only a part of
+    the page width is used to render this signature. If two or more
+    signatures are passed in, the complete page width will be filled
+    and the single signature cells will be shrinked to fit.
+    """
+    data_list = get_sig_tables(
+        signatures, lang=lang, max_per_row=max_per_row,
+        horizontal=horizontal, single_table=single_table, landscape=landscape)
+    return [Table(row_data, style=row_style, colWidths=row_col_widths,
+                  repeatRows=4)
+            for row_data, row_style, row_col_widths in data_list]
+
+def format_signatures(signatures, max_per_row=3, lang='en',
+                      single_table=False,
+                      date_field=True, date_text=_('Date'),
+                      base_style=SIGNATURE_TABLE_STYLE):
+    result = []
+    signature_tuples = [normalize_signature(sig) for sig in signatures]
+    for pre, sig, post in signature_tuples:
+        row = []
+        if pre is not None:
+            row.append([
+                translate(pre, lang), '', '', ''])
+        row.append([
+            translate(_('Date'), lang), '',
+            translate(sig, lang), ''
+            ])
+        if post is not None:
+            row.append([
+                translate(post, lang), '', '', ''])
+        result.append((row, base_style))
+    return result
+
+
+
+class NumberedCanvas(Canvas):
+    """A reportlab canvas for numbering pages after all docs are processed.
+
+    Taken from
+    http://code.activestate.com/recipes/546511-page-x-of-y-with-reportlab/
+    http://code.activestate.com/recipes/576832/
+    """
+
+    def __init__(self, *args, **kw):
+        Canvas.__init__(self, *args, **kw)
+        self._saved_page_states = []
+        return
+
+    def showPage(self):
+        self._saved_page_states.append(dict(self.__dict__))
+        self._startPage()
+        return
+
+    def save(self):
+        """add page info to each page (page x of y)"""
+        num_pages = len(self._saved_page_states)
+        for state in self._saved_page_states:
+            self.__dict__.update(state)
+            self.draw_page_number(num_pages)
+            Canvas.showPage(self)
+        Canvas.save(self)
+        return
+
+    def draw_page_number(self, page_count):
+        """draw string at bottom right with 'page x of y'.
+
+        Location of the string is determined by canvas attributes
+        `kofa_footer_x_pos` and `kofa_footer_y_pos` that have to be
+        set manually.
+
+        If this canvas also provides an attribute `kofa_footer_text`,
+        the contained text is rendered left of the ``page x of y``
+        string.
+        """
+        self.setFont("Helvetica", 9)
+        right_footer_text = _(
+            '${footer_text} Page ${num1} of ${num2}',
+            mapping = {'footer_text': self.kofa_footer_text,
+                       'num1':self._pageNumber, 'num2':page_count})
+        self.drawRightString(
+            self.kofa_footer_x_pos, self.kofa_footer_y_pos,
+             translate(right_footer_text))
+        return
+
+class PDFCreator(grok.GlobalUtility):
+    """A utility to help with generating PDF docs.
+    """
+    grok.implements(IPDFCreator)
+
+    watermark_path = None
+    header_logo_path = None
+    header_logo_left_path = None
+    watermark_pos = [0, 0]
+    logo_pos = [0, 0, 0]
+    logo_left_pos = [0, 0, 0]
+    pagesize = portrait(A4)
+
+    @classmethod
+    def _setUpWidgets(cls, form_fields, context):
+        """Setup simple display widgets.
+
+        Returns the list of widgets.
+        """
+        request = TestRequest()
+        return setUpEditWidgets(
+            form_fields, 'form', context, request, {},
+            for_display=True, ignore_request=True
+            )
+
+    @classmethod
+    def _addCourse(cls, table_data, row_num, course_label, course_link,
+                   lang, domain):
+        """Add course data to `table_data`.
+        """
+        if not course_label or not course_link:
+            return table_data, row_num
+        f_label= translate(
+            _(course_label), domain, target_language=lang)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = Paragraph(course_link, ENTRY1_STYLE)
+        table_data.append([f_label, f_text])
+        row_num += 1
+        return table_data, row_num
+
+    @classmethod
+    def _addDeptAndFaculty(cls, table_data, row_num, dept, faculty,
+                           lang, domain):
+        """Add `dept` and `faculty` as table rows to `table_data`.
+
+        `dept` and `faculty` are expected to be strings or None. In
+        latter case they are not put into the table.
+        """
+        for label, text in (('Department:', dept), ('Faculty:', faculty)):
+            if text is None:
+                continue
+            label = translate(_(label), domain, target_language=lang)
+            table_data.append([
+                Paragraph(label, ENTRY1_STYLE),
+                Paragraph(text, ENTRY1_STYLE)])
+            row_num += 1
+        return table_data, row_num
+
+    @classmethod
+    def _drawSignatureBoxes(cls, canvas, width, height, signatures=[]):
+        """Draw signature boxes into canvas.
+        """
+        canvas.saveState()
+        canvas.setFont("Helvetica", 10)
+        mystring = "%r" % ([translate(sig) for sig in signatures])
+        for num, sig in enumerate(signatures):
+            box_width = (width - 4.2*cm) / len(signatures)
+            x_box = 2.1*cm + (box_width) * num
+            y_box = 0.75*inch
+            canvas.rect(x_box+0.1*cm, y_box, box_width-0.2*cm, 0.75*inch)
+            canvas.drawString(
+                x_box+0.2*cm, 1.35*inch, translate(sig))
+        canvas.restoreState()
+        return canvas
+
+    @classmethod
+    def fromStringList(cls, string_list):
+        """Generate a list of reportlab paragraphs out of a list of strings.
+
+        Strings are formatted with :data:`CODE_STYLE` and a spacer is
+        appended at end.
+        """
+        result = []
+        for msg in string_list:
+            result.append(Paragraph(msg, CODE_STYLE))
+        return result
+
+    @classmethod
+    def getImage(cls, image_path, orientation='LEFT'):
+        """Get an image located at `image_path` as reportlab flowable.
+        """
+        img = Image(image_path, width=4*cm, height=3*cm, kind='bound')
+        img.hAlign=orientation
+        return img
+
+    def _getWidgetsTableData(self, widgets, separators, domain,
+                             lang, twoDataCols):
+        row_num = 0
+        table_data = []
+        for widget in widgets:
+            if separators and separators.get(widget.name):
+                f_headline = translate(
+                    separators[widget.name], domain, target_language=lang)
+                f_headline = Paragraph(f_headline, HEADLINE1_STYLE)
+                table_data.append([f_headline])
+                row_num += 1
+            f_label = translate(widget.label.strip(), domain,
+                                target_language=lang)
+            f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
+            f_text = translate(widget(), domain, target_language=lang)
+            f_text = format_html(f_text)
+            #if f_text:
+            #    hint = ' <font size=9>' + widget.hint + '</font>'
+            #    f_text = f_text + hint
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            table_data.append([f_label,f_text])
+            row_num += 1
+        return table_data, row_num
+
+    def getWidgetsTable(self, form_fields, context, view, lang='en',
+                        domain='waeup.kofa', separators=None,
+                        course_label=None, course_link=None, dept=None,
+                        faculty=None, colWidths=None, twoDataCols=False):
+        """Return a reportlab `Table` instance, created from widgets
+        determined by `form_fields` and `context`.
+
+        - `form_fields`
+           is a list of schema fields as created by grok.AutoFields.
+        - `context`
+           is some object whose content is rendered here.
+        - `view`
+           is currently not used but supposed to be a view which is
+           actually rendering a PDF document.
+        - `lang`
+           the portal language. Used for translations of strings.
+        - `domain`
+           the translation domain used for translations of strings.
+        - `separators`
+           a list of separators.
+        - `course_label` and `course_link`
+           if a course should be added to the table, `course_label`
+           and `course_link` can be given, both being strings. They
+           will be rendered in an extra-row.
+        - `dept` and `faculty`
+           if these are given, we render extra rows with faculty and
+           department.
+        - `colWidths`
+           defines the the column widths of the data in the right column
+           of base data (right to the passport image).
+        - `twoDataCols`
+           renders data widgets in a parent table with two columns.
+        """
+        table_style = [('VALIGN', (0,0), (-1,-1), 'TOP'),
+                       ]
+        widgets = self._setUpWidgets(form_fields, context)
+
+        # Determine table data
+        table_data, row_num = self._getWidgetsTableData(
+            widgets, separators, domain, lang, twoDataCols)
+
+        # Add course (admitted, etc.) if applicable
+        table_data, row_num = self._addCourse(
+            table_data, row_num, course_label, course_link, lang, domain,)
+
+        ## Add dept. and faculty if applicable
+        table_data, row_num = self._addDeptAndFaculty(
+            table_data, row_num, dept, faculty, lang, domain)
+
+        # render two-column table of tables if applicable
+        lines = len(table_data)
+        middle = lines/2
+        if twoDataCols is True and lines > 2:
+            table_left = Table(table_data[:middle],
+                               style=table_style, colWidths=[3*cm, 6.3*cm])
+            table_right = Table(table_data[middle:],
+                                style=table_style, colWidths=[3*cm, 6.3*cm])
+            table_style.append(('LEFTPADDING', (0,0), (0,-1), 1.2*cm),)
+            table = Table([[table_left, table_right],],style=table_style)
+            return table
+
+        # render single table
+        table = Table(
+            table_data,style=table_style, colWidths=colWidths) #, rowHeights=14)
+        table.hAlign = 'LEFT'
+        return table
+
+
+    def paint_background(self, canvas, doc):
+        """Paint background of a PDF, including watermark, title, etc.
+
+        The `doc` is expected to be some reportlab SimpleDocTemplate
+        or similar object.
+
+        Text of headerline is extracted from doc.kofa_headtitle, the
+        document title (under the head) from doc.kofa_title.
+
+        This is a callback method that will be called from reportlab
+        when creating PDFs with :meth:`create_pdf`.
+        """
+        canvas.saveState()
+        width, height = doc.width, doc.height
+        width += doc.leftMargin + doc.rightMargin
+        height += doc.topMargin + doc.bottomMargin
+
+        # Watermark
+        if self.watermark_path is not None:
+            canvas.saveState()
+            canvas.drawImage(self.watermark_path,
+                self.watermark_pos[0], self.watermark_pos[1])
+            canvas.restoreState()
+
+        # Header
+        site_config = None
+        site = grok.getSite()
+        if site is not None:
+            site_config = site.get('configuration', None)
+        head_title = getattr(
+            doc, 'kofa_headtitle', getattr(
+                site_config, 'name',
+                u'Sample University'))
+        canvas.setFont("Helvetica-Bold", 18)
+        if self.header_logo_left_path is not None:
+            canvas.drawCentredString(width/2.0, height-1.7*cm, head_title)
+        else:
+            canvas.drawString(1.5*cm, height-1.7*cm, head_title)
+        canvas.line(1.5*cm,height-1.9*cm,width-1.5*cm,height-1.9*cm)
+        if self.header_logo_path is not None:
+            canvas.drawImage(self.header_logo_path,
+                self.logo_pos[0], self.logo_pos[1], width=self.logo_pos[2],
+                preserveAspectRatio=True, anchor='ne')
+        if self.header_logo_left_path is not None:
+            canvas.drawImage(self.header_logo_left_path,
+                self.logo_left_pos[0], self.logo_left_pos[1],
+                width=self.logo_left_pos[2],
+                preserveAspectRatio=True, anchor='ne')
+
+        # Title
+        canvas.saveState()
+        canvas.setFont("Helvetica-Bold", 14)
+        title = getattr(doc, 'kofa_title', '')
+        if '\n' in title:
+            title_lines = title.split('\n')
+            for num, line in enumerate(title_lines):
+                canvas.drawCentredString(
+                    width/2.0, height-2.8*cm-(num*0.7*cm), line)
+        elif title:
+            canvas.drawCentredString(width/2.0, height-2.8*cm, title)
+        canvas.restoreState()
+
+        # Footer
+        canvas.saveState()
+        if getattr(doc, 'sigs_in_footer', False):
+            self._drawSignatureBoxes(
+                canvas, width, height, doc.sigs_in_footer)
+        canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
+        canvas.setFont("Helvetica", 9)
+        if not getattr(doc, 'kofa_nodate', False):
+            tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+            canvas.drawString(2.2*cm, 0.5 * inch,
+                translate(_(u'Date: ${a}', mapping = {'a': today})))
+        # set canves attributes needed to render `page x of y`
+        canvas.kofa_footer_x_pos = width-2.2*cm
+        canvas.kofa_footer_y_pos = 0.5 * inch
+        canvas.kofa_footer_text =  doc.kofa_footer
+        canvas.restoreState()
+
+        # Metadata
+        canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
+        canvas.setSubject(title)
+        canvas.setCreator(u'WAeUP Kofa')
+        return
+
+    def create_pdf(self, data, headerline=None, title=None, author=None,
+                   footer='', note=None, sigs_in_footer=[], topMargin=1.5,
+                   letterhead_path=None):
+        """Returns a binary data stream which is a PDF document.
+        """
+        pdf_stream = StringIO()
+        bottomMargin = len(sigs_in_footer) and 1.9*inch or 1.2*inch
+        if not letterhead_path:
+            topMargin += title.count('\n') * 0.2
+        doc = SimpleDocTemplate(
+            pdf_stream,
+            bottomMargin=bottomMargin,
+            topMargin=topMargin*inch,
+            title=title,
+            pagesize=self.pagesize,
+            showBoundary=False,
+            )
+        # Set some attributes that are needed when rendering the background.
+        if headerline is not None:
+            doc.kofa_headtitle = headerline
+        doc.kofa_title = title
+        doc.kofa_author = author
+        doc.kofa_footer = footer
+        doc.sigs_in_footer = sigs_in_footer
+        doc.letterhead_path = letterhead_path
+        if note is not None:
+            html = format_html(note)
+            data.append(Paragraph(html, NOTE_STYLE))
+        doc.build(data, onFirstPage=self.paint_background,
+                  onLaterPages=self.paint_background,
+                  canvasmaker=NumberedCanvas
+                  )
+        result = pdf_stream.getvalue()
+        pdf_stream.close()
+        return result
+
+class LandscapePDFCreator(PDFCreator):
+    """A utility to help with generating PDF docs in
+    landscape format.
+    """
+    grok.name('landscape')
+    pagesize = landscape(A4)
+
+class LetterPDFCreator(PDFCreator):
+    """A utility to help with generating PDF docs with an original letterhead.
+    """
+    grok.name('letter')
+    letterhead_pos = [0, 0]
+
+    def paint_background(self, canvas, doc):
+        """Paint letterhead background of a PDF.
+
+        The `doc` is expected to be some reportlab SimpleDocTemplate
+        or similar object.
+
+        This is a callback method that will be called from reportlab
+        when creating PDFs with :meth:`create_pdf`.
+        """
+        canvas.saveState()
+        width, height = doc.width, doc.height
+        width += doc.leftMargin + doc.rightMargin
+        height += doc.topMargin + doc.bottomMargin
+
+        # Letterhead
+        if doc.letterhead_path is not None:
+            canvas.saveState()
+            canvas.drawImage(doc.letterhead_path,
+                self.letterhead_pos[0], self.letterhead_pos[1],
+                width=width, height=height)
+            canvas.restoreState()
+
+        # Footer
+        if getattr(doc, 'sigs_in_footer', False):
+            self._drawSignatureBoxes(
+                canvas, width, height, doc.sigs_in_footer)
+        #canvas.line(2.2*cm, 0.62*inch, width-2.2*cm, 0.62*inch)
+        canvas.setFont("Helvetica", 9)
+        if not getattr(doc, 'kofa_nodate', False):
+            tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+            today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+            #canvas.drawString(2.2*cm, 0.5 * inch,
+            #    translate(_(u'Date: ${a}', mapping = {'a': today})))
+            canvas.drawString(2.2*cm, 0.3 * inch,
+                translate(_(u'Date: ${a}', mapping = {'a': today})))
+        # set canves attributes needed to render `page x of y`
+        canvas.kofa_footer_x_pos = width-2.2*cm
+        #canvas.kofa_footer_y_pos = 0.5 * inch
+        canvas.kofa_footer_y_pos = 0.3 * inch
+        canvas.kofa_footer_text =  doc.kofa_footer
+        canvas.restoreState()
+
+        # Metadata
+        title = getattr(doc, 'kofa_title', '')
+        canvas.setAuthor(getattr(doc, 'kofa_author', 'Unknown'))
+        canvas.setSubject(title)
+        canvas.setCreator(u'WAeUP Kofa')
+        return
+
+
+class A3LandscapePDFCreator(LandscapePDFCreator):
+    """A utility to help with generating PDF docs in
+    A3 landscape format. No watermark is shown.
+    """
+    grok.name('A3landscape')
+    pagesize = landscape(A3)
+
+def get_qrcode(text, width=60.0):
+    """Get a QR Code as Reportlab Flowable (actually a `Drawing`).
+
+    `width` gives box width in pixels (I think)
+    """
+    widget = QrCodeWidget(text)
+    bounds = widget.getBounds()
+    w_width = bounds[2] - bounds[0]
+    w_height = bounds[3] - bounds[1]
+    drawing = Drawing(
+        width, width,
+        transform=[width/w_width, 0, 0, width/w_height, 0, 0])
+    drawing.add(widget)
+    return drawing
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/reports.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/reports.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/reports.py	(revision 16045)
@@ -0,0 +1,183 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Browser components for report generation.
+"""
+import grok
+from datetime import datetime, timedelta
+from zope.i18n import translate
+from zope.component import getUtility, queryUtility
+from zope.location.location import located
+from zope.security import checkPermission
+from waeup.kofa.interfaces import IJobManager, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.layout import KofaPage, jsaction
+from waeup.kofa.utils.helpers import get_current_principal, get_user_account
+from waeup.kofa.reports import (
+    IReportsContainer, IReportGenerator, get_generators)
+
+
+grok.templatedir('templates')
+
+
+class ReportsContainerPage(KofaPage):
+    """A view on a reports container.
+    """
+    grok.name('index')
+    grok.context(IReportsContainer)
+    grok.require('waeup.handleReports')
+    label = _('Reports')
+
+    def _report_url(self, job_id):
+        """Get the PDF download URL of a report.
+        """
+        return self.url(self.context, '%s/pdf' % job_id)
+
+    def update(self, job_id=None, DISCARD=None, DOWNLOAD=None, PURGE=None):
+        self.entries = []
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
+        if job_id and DISCARD:
+            entry = self.context.report_entry_from_job_id(job_id)
+            self.context.delete_report_entry(entry)
+            self.flash('Report %s discarded' % job_id)
+            grok.getSite().logger.info(
+                '%s - report %s discarded' % (ob_class, job_id))
+        if not checkPermission('waeup.manageReports', self.context):
+            user_id = get_current_principal().id
+        else:
+            user_id = None
+        self.entries = self._generate_entries(user_id=user_id)
+        if job_id and DOWNLOAD:
+            self.redirect(self._report_url(job_id))
+            return
+        if PURGE:
+            counter = 0
+            for entry in self.context.get_running_report_jobs(user_id=user_id):
+                job_id, gen_name, user = entry
+                job = getUtility(IJobManager).get(job_id)
+                starttime = getattr(job, 'begin_after', None)
+                delta = timedelta(weeks=4)
+                tz = getUtility(IKofaUtils).tzinfo
+                if datetime.now(tz) - delta > starttime:
+                    self.context.delete_report_entry(entry)
+                    counter += 1
+                    grok.getSite().logger.info(
+                        '%s - report %s purged' % (ob_class, job_id))
+            self.flash('%s report(s) purged.' % counter)
+            self.redirect(self.url(self.context))
+        return
+
+    def _generate_entries(self, user_id=None):
+        entries = []
+        for entry in self.context.get_running_report_jobs(user_id=user_id):
+            job_id, gen_name, user = entry
+            job = getUtility(IJobManager).get(job_id)
+            generator = queryUtility(IReportGenerator, name=gen_name)
+            gen_title = translate(getattr(generator, 'title', _('Unknown')))
+            # Sort arguments
+            sorted_items = sorted(
+                job.kwargs['kw'].items(), key=lambda value:value[0])
+            sorted_values = [i[1] for i in sorted_items]
+            arguments = ', '.join([str(x) for x in sorted_values])
+            descr = '%s (%s)' % (gen_title, arguments)
+            status = job.finished and 'ready' or 'running'
+            status = job.failed and 'FAILED' or status
+            starttime = getattr(job, 'begin_after', None)
+            if starttime:
+                starttime = starttime.astimezone(
+                    getUtility(
+                        IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+            new_entry = (job_id, descr, status, job.finished, job.finished \
+                and not job.failed, not job.finished, starttime, user)
+            entries.append(new_entry)
+        return entries
+
+class ReportsContainerTraverser(grok.Traverser):
+    """A traverser for reports containers.
+    """
+    grok.context(IReportsContainer)
+
+    def traverse(self, name):
+        """Return a report generator or report if one is registered under
+        `name`.
+
+        Generators are registered by their utility names while reports
+        are looked up by their job id. So, `name` must be a report
+        generator name or a valid job_id of a report job.
+        """
+        generators = dict(list(get_generators()))
+        result = generators.get(name, None)
+        if result:
+            # give generator a location in URLs (make url() work)
+            return located(result, self.context, name)
+        result = self.context.report_entry_from_job_id(name)
+        if result:
+            manager = getUtility(IJobManager)
+            job = manager.get(name)
+            report = job.result
+            return located(report, self.context, name)
+        return None
+
+
+class ReportsContainerCreate(KofaPage):
+    """Create a new report.
+    """
+    grok.name('create')
+    grok.context(IReportsContainer)
+    grok.require('waeup.handleReports')
+    label = _('Create report')
+
+    locally_allowed_reports = (
+        'level_report', 'raw_score_report', 'session_results_presentation')
+
+    def update(self, START_GENERATOR=None, generator=None):
+        utils = queryUtility(IKofaUtils)
+        if not utils.expensive_actions_allowed():
+            self.flash(_(
+                "Currently, new reports cannot be created due to high "
+                "system load. Please try again later."), type='danger')
+            self.redirect(self.url(self.context))
+            return
+        self.creators = self.get_creators()
+        self.generator_names = [x[1] for x in self.creators]
+        if START_GENERATOR and generator and generator in self.generator_names:
+            self.redirect(self.url(self.context, generator))
+        pass
+
+    def get_creators(self):
+        """Get all registered report generator names.
+
+        Returns a list of tuples (<TITLE>, <NAME>) with ``<TITLE>``
+        being a human readable description of the respective generator
+        and ``<NAME>`` being the registration name with the ZCA.
+
+        If `waeup.local.ReportsOfficer` role has been assigned at
+        department level, list only report generator which allow
+        to filter department data.
+        """
+        try:
+            local_roles = get_user_account(self.request).getLocalRoles()
+        except AttributeError:  # Managers have no user account
+            local_roles = {}
+        lror = local_roles.get('waeup.local.ReportsOfficer', None)
+        if lror:
+            result = [(gen.title, name) for name, gen in get_generators()
+                                        if name in self.locally_allowed_reports]
+        else:
+            result = [(gen.title, name) for name, gen in get_generators()]
+        sorted_result = sorted(result, key=lambda value:value[1])
+        return sorted_result
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/actionbutton.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/actionbutton.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/actionbutton.pt	(revision 16045)
@@ -0,0 +1,7 @@
+<a class="btn btn-default" tal:condition="viewlet/target_url"
+   tal:attributes="href viewlet/target_url; onclick viewlet/onclick">
+  <img src="" alt="edit"
+   tal:attributes="src viewlet/icon_url;
+	   alt viewlet/alt"/>
+  <span tal:replace="viewlet/text">TEXT</span>
+</a>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/addaction.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/addaction.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/addaction.pt	(revision 16045)
@@ -0,0 +1,10 @@
+<span class="yui-button">
+  <span class="first-child">
+    <a href=""
+       tal:attributes="href viewlet/getTargetURL"
+       id="linkbutton1">
+      <img alt="edit"
+           src="http://localhost:8080/app/@@/waeup/actionicon_addfavorite.png" />
+    <span tal:content="viewlet/addlink_text">Add something</span></a>
+  </span>
+</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/administrationpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/administrationpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/administrationpage.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<p>
+  You can manage the following tasks:
+</p>
+
+<tal:admintasks tal:content="structure provider:admintasks" />
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/admintask.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/admintask.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/admintask.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<p>
+<a href=""
+   tal:attributes="href viewlet/link_target"
+   tal:content="viewlet/link_title">Manage Users</a>
+</p>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/breadcrumbs.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/breadcrumbs.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/breadcrumbs.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<li tal:repeat="item viewlet/getEntries">
+  <a href="#" tal:attributes="href item/url">
+    <span tal:replace="item/title">Entry</span>
+  </a>
+</li>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatecoursepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatecoursepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatecoursepage.pt	(revision 16045)
@@ -0,0 +1,41 @@
+<table i18n:domain="waeup.kofa" class="form-table">
+  <thead>
+  </thead>
+  <tbody>
+    <tr>
+      <td i18n:translate="">Code:</td>
+      <td tal:content="context/__name__">CODE</td>
+    </tr>
+    <tr>
+      <td i18n:translate="">Course Code:</td>
+      <td tal:content="context/getCourseCode">THE COURSE</td>
+    </tr>
+    <tr>
+      <td i18n:translate="">Course Title:</td>
+      <td tal:content="context/course/title">THE TITLE</td>
+    </tr>
+    <tr>
+      <td class="fieldname" i18n:translate="">Provided by:</td>
+      <td>
+      <span tal:content="python: context.course.__parent__.__parent__.longtitle">DEPARTMENT</span>
+      <br />
+      <span tal:content="python: context.course.__parent__.__parent__.__parent__.longtitle">FACULTY</span>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">Level:</td>
+      <td tal:content="view/leveltitle">LEVEL</td>
+    </tr>
+    <tr tal:condition="layout/course_cat_used">
+      <td i18n:translate="">Course Category:</td>
+      <td tal:content="view/course_category_title">NONE</td>
+    </tr>
+    <tr>
+      <td i18n:translate="">Required course:</td>
+      <td tal:content="context/mandatory">REQUIRED</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatemanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatemanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatemanagepage.pt	(revision 16045)
@@ -0,0 +1,147 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Settings</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab"
+         i18n:translate="">Certificate Courses</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th>
+            <th i18n:translate="">Level</th>
+            <th i18n:translate="">Semester</th>
+            <th i18n:translate="">Cert. Course</th>
+            <th i18n:translate="">Dep. Course</th>
+            <th i18n:translate="">Title</th>
+            <th i18n:translate="" tal:condition="layout/course_cat_used">Course Cat.</th>
+            <th i18n:translate="">Required</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="cc context/values">
+            <td>
+              <input type="checkbox" name="val_id"
+                     tal:attributes="value cc/__name__" />
+            </td>
+            <td>
+              <span tal:content="cc/level">LEVEL</span>
+            </td>
+            <td tal:content="cc/course/semester">SEMESTER</td>
+            <td>
+              <a tal:attributes="href python: view.url(cc)"
+                 tal:content="cc/__name__">
+            	REFERRER CODE
+              </a>
+            </td>
+            <td>
+              <a tal:attributes="href python:view.url(cc.course)"
+                 tal:content="cc/course/code">COURSE CODE</a>
+            </td>
+            <td>
+              <span tal:content="cc/course/title">COURSE TITLE</span>
+            </td>
+            <td tal:condition="layout/course_cat_used">
+              <span tal:content="cc/course_category">COURSE CAT</span>
+            </td>
+            <td>
+              <span tal:content="cc/mandatory">REQUIRED</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th>
+            <th i18n:translate="">User Id</th>
+            <th i18n:translate="">Name</th>
+            <th i18n:translate="">Local Role</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <input id="filterbox" class="kofa-filterbox form-control half"
+             placeholder="Enter search text, then select item below."
+             type="text" i18n:translate=""/>
+      <br />
+      <div class="form-inline">
+        <div class="form-group">
+          <select id="filteredselect" name="user"
+                  class="form-control" size=5>
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+              <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+            <span tal:repeat="action view/actions" tal:omit-tag="">
+              <input tal:condition="python:action.label in view.tabthreeactions2"
+                     tal:replace="structure action/render"/>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/certificatepage.pt	(revision 16045)
@@ -0,0 +1,73 @@
+<table class="form-table">
+<thead>
+</thead>
+<tbody>
+  <tal:block repeat="widget view/widgets">
+    <tr>
+      <td class="fieldname">
+        <span tal:content="widget/label">label</span>:
+      </td>
+      <td tal:on-error="default">
+        <div tal:content="structure widget">
+          <input type="text" />
+        </div>
+      </td>
+    </tr>
+  </tal:block>
+</tbody>
+</table>
+
+<h3>
+  <span tal:content="context/__name__">Code</span>
+  <span i18n:domain="waeup.kofa" i18n:translate="">Courses</span>
+</h3>
+<br />
+<table i18n:domain="waeup.kofa" class="kofa-data-table dataTable">
+<thead>
+  <tr>
+    <th i18n:translate="">Level</th>
+    <th i18n:translate="">Semester</th>
+    <th i18n:translate="">Cert. Course</th>
+    <th i18n:translate="">Dep. Course</th>
+    <th i18n:translate="">Title</th>
+    <th i18n:translate="" tal:condition="layout/course_cat_used">Course Cat.</th>
+    <th i18n:translate="">Required</th>
+  </tr>
+</thead>
+<tbody>
+  <tr tal:repeat="cc context/values">
+     <td>
+      <span tal:content="cc/level">
+        LEVEL
+      </span>
+    </td>
+    <td tal:content="cc/course/semester">SEMESTER</td>
+    <td>
+      <a tal:attributes="href python: view.url(cc)"
+        tal:content="cc/__name__">
+        REFERRER CODE
+      </a>
+    </td>
+    <td>
+      <a tal:attributes="href python:view.url(cc.course)"
+       tal:content="cc/course/code">
+      COURSE CODE
+      </a>
+    <td>
+      <span tal:content="cc/course/title">
+      COURSE TITLE
+      </span>
+    </td>
+    <td tal:condition="layout/course_cat_used">
+      <span tal:content="cc/course_category">
+        COURSE CAT
+      </span>
+    </td>
+    <td>
+      <span tal:content="cc/mandatory">
+        REQUIRED
+      </span>
+    </td>
+  </tr>
+</tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/changepw.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/changepw.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/changepw.pt	(revision 16045)
@@ -0,0 +1,26 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <br />
+  <p tal:condition="not: view/auto_fill"
+     i18n:translate="email_address_problem">
+    Enter the email address stored in Kofa.
+    You can't change your email address here.
+    If you've forgotten your email address or the email
+    address stored in Kofa is wrong or no longer valid,
+    use the enquiries form to request a change
+    of address.
+  </p>
+  <div tal:content="structure view/captcha_code"></div>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/configurationmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/configurationmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/configurationmanagepage.pt	(revision 16045)
@@ -0,0 +1,66 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab"
+         i18n:translate="">Site Settings</a>
+    </li>
+    <li>
+      <a href="#tab2"  data-toggle="tab"
+         i18n:translate="">Session Configurations</a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+          <tr>
+            <td>Mandates:</td>
+            <td tal:define="count python:layout.site['mandates'].count">
+              <span tal:content="python:count[0]"></span>
+              <span i18n:translate="">active</span>,
+              <span tal:content="python:count[1]"></span>
+              <span i18n:translate="">expired</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <br />
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Name
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getSessionConfigurations">
+            <td>
+            <input type="checkbox" name="val_id"
+                   tal:attributes="value entry/name" /> </td> <td>
+            <a href="" tal:attributes="href entry/url"
+               tal:content="entry/title">Name</a> </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div><!-- /tab2 -->
+  </div><!-- /tab-content -->
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/contactform.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/contactform.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/contactform.pt	(revision 16045)
@@ -0,0 +1,30 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <tal:block repeat="widget view/widgets">
+
+    <div tal:condition="python: widget.name != 'form.body'"
+         tal:content="widget/label">Label
+    </div>
+    <br />
+    <div tal:content="structure widget">
+      Textbody
+    </div>
+    <tal:error tal:condition="widget/error">
+      <span tal:replace="structure widget/error">error</span>
+      <br />
+    </tal:error>
+    <br />
+  </tal:block>
+
+  <tal:captcha condition="view/captcha_code|nothing">
+    <p tal:content="structure view/captcha_code"></p>
+    <br />
+  </tal:captcha>
+
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/coursemanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/coursemanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/coursemanagepage.pt	(revision 16045)
@@ -0,0 +1,92 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Settings</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <input id="filterbox" class="kofa-filterbox form-control half"
+             placeholder="Enter search text, then select item below."
+             type="text" i18n:translate=""/>
+      <br />
+      <div class="form-inline">
+        <div class="form-group">
+          <select id="filteredselect" name="user"
+                  class="form-control" size=5>
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+              <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabtwoactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterexportpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 16045)
@@ -0,0 +1,114 @@
+<p i18n:translate="">
+  Here you can create CSV files from parts of portal data except student data.
+  Please pick the type of objects you want to export from the
+  selection below. The file will be generated and then be
+  made available for you to download in the table below.
+  Files can only be downloaded within 24 hours after generation.
+  The download link disappears afterwards.
+</p>
+
+<br />
+
+<form  method="POST" class="form-inline">
+  <label for="exporter">
+    Exporter (Data Type):
+  </label>
+  <div class="form-group">
+    <select name="exporter" class="form-control half">
+      <span tal:repeat="items view/getExporters" tal:omit-tag="">
+        <option tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE
+        </span>
+        </option>
+      </span>
+    </select>
+  </div>
+  <div class="form-group">
+    <input i18n:translate="" type="submit" class="btn btn-primary"
+      name="CREATE" tal:attributes="value view/export_button" />
+  </div>
+  <div class="form-group">
+  <input type="submit" name="CANCEL" class="btn btn-default"
+      tal:attributes="value view/cancel_button"
+      />
+  </div>
+</form>
+
+<br />
+
+<p i18n:translate="">
+  Student data exports have to be further configured. You can either refine
+  the results by <a href="@@exportconfig">selecting certain export parameters</a>
+  or by <a href="@@exportselected">entering a set of student ids
+  or matric numbers</a>.
+</p>
+
+<br /><br />
+
+<table i18n:domain="waeup.kofa" class="table table-condensed table-hover">
+  <thead>
+    <tr>
+      <th i18n:translate="">Export Number</th>
+      <th i18n:translate="">Exporter</th>
+      <th i18n:translate="">Filter</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="job view/entries">
+      <td>
+        <span tal:replace="job/job">12</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/exporter">EXPORTER</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/args">ARGS</span>
+      </td>
+      <td>
+        <span tal:replace="job/creator">CREATOR</span>
+      </td>
+      <td>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn btn-default btn-xs" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img src="/static/img/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn btn-primary btn-xs"
+                 tal:attributes="href job/download_url"
+                 tal:condition="job/show_download_button"
+                 i18n:translate="">
+            Download</a>
+          <input type="hidden" name="job_id"
+                 tal:attributes="value job/job" />
+          <input type="submit" class="btn btn-default btn-xs"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button"
+                 i18n:translate=""/>
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<form method="POST" i18n:domain="waeup.kofa">
+  <div class="form-group">
+    <input type="submit" class="btn btn-primary"
+      name="PURGE" value="Purge outdated exports"
+      data-toggle="tooltip"
+      title="All exports older than 24 hours will be discarded!"
+      i18n:translate=""
+      />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt	(revision 16045)
@@ -0,0 +1,27 @@
+<p i18n:translate="">
+  Find below the last
+  <span tal:content="view/context/max_files" i18n:name="count">50</span>
+  imports. The files contain the imported datasets.
+</p>
+<table i18n:domain="waeup.kofa" class="kofa-data-table dataTableFiles">
+  <thead>
+    <tr>
+      <th i18n:translate="">File</th>
+      <th i18n:translate="">Datasets</th>
+      <th i18n:translate="">Upload Date</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="file context/getFinishedFiles">
+      <td><a tal:attributes="href python:  view.url(context)  +
+                             '/download?filename=' + file.rel_path"
+             tal:content="file/name">FILENAME</a></td>
+      <td tal:content="python: file.lines-1">12 k</td>
+      <td tal:content="file/uploaddate">Mar 12, 2008</td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST">
+  <input i18n:translate="" type="submit" class="btn btn-default" name="CANCEL"
+      tal:attributes="value view/cancel_button"/>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport1page.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport1page.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport1page.pt	(revision 16045)
@@ -0,0 +1,34 @@
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Step 1</h3>
+<p i18n:domain="waeup.kofa" i18n:translate="">
+  Using batch processing you can mass-create, mass-update, or
+  mass-remove datasets from the database using CSV files.
+</p>
+<p i18n:domain="waeup.kofa" i18n:translate="">
+  Please select a file for processing from the list below.
+</p>
+<form i18n:domain="waeup.kofa" method="POST">
+  <table class="table">
+    <thead>
+      <th />
+      <th i18n:translate="">Filename</th>
+      <th i18n:translate="">Datasets</th>
+      <th i18n:translate="">Size</th>
+      <th i18n:translate="">Date</th>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file view/getFiles">
+        <td>
+          <button i18n:translate=""
+            type="submit" name="select" class="btn btn-primary"
+            tal:attributes="value file/name">Select</button>
+        </td>
+        <td tal:content="file/name">Filename.csv</td>
+        <td tal:content="python: file.lines - 1">12</td>
+        <td tal:content="file/size">123 Bytes</td>
+        <td tal:content="file/getDate">1.1.1970</td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" name="cancel" class="btn btn-default"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport2page.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport2page.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport2page.pt	(revision 16045)
@@ -0,0 +1,60 @@
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Step 2</h3>
+<form i18n:domain="waeup.kofa" method="POST">
+  <p>
+    <b i18n:translate="">File:</b>
+    <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename" tal:attributes="value view/filename" />
+  </p>
+  <p i18n:translate="">
+    Please select a file-processor and a processing-mode from the selections below.
+  </p>
+  <table tal:define="prev_table view/getPreviewTable;" class="table table-condensed">
+    <thead>
+      <tr>
+        <th i18n:translate="">Header</th>
+        <th i18n:translate="">Sample Record 1</th>
+        <th i18n:translate="">Sample Record 2</th>
+        <th i18n:translate="">Sample Record 3</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="row prev_table">
+        <td tal:repeat="value row"
+            tal:content="value"> value
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <div> <b i18n:translate="">Processor:</b>
+    <select name="importer" class="form-control">
+      <option tal:repeat="importer view/getImporters"
+          tal:attributes="value importer/name;
+              selected python: importer['name'] == view.importer and 'selected' or ''"
+          tal:content="importer/title"> Some Importer
+      </option>
+    </select>
+  </div>
+  <p>&nbsp;
+  </p>
+  <div> <b i18n:translate="">Mode:</b>
+    <input type="radio" name="mode" value="create" checked="checked"
+       tal:attributes="checked python: view.mode == 'create' and 'checked' or '';
+        disabled python: view.mode != 'create' and view.mode_locked and 'disabled' or ''" />
+        Create  &nbsp;&nbsp;
+    <input type="radio" name="mode" value="update"
+       tal:attributes="checked python: view.mode == 'update' and 'checked' or '';
+        disabled python: view.mode != 'update' and view.mode_locked and 'disabled' or ''" />
+        Update  &nbsp;&nbsp;
+    <input type="radio" name="mode" value="remove"
+       tal:attributes="checked python: view.mode == 'remove' and 'checked' or '';
+        disabled python: view.mode != 'remove' and view.mode_locked and 'disabled' or ''" />
+        Remove
+    <p>&nbsp;
+    </p>
+  </div>
+  <input class="btn btn-default" type="submit" name="cancel" tal:attributes="value view/cancel_button" />
+  <input class="btn btn-danger" type="submit" name="back1" tal:attributes="value view/back_button" />
+  <input class="btn btn-primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport3page.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport3page.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport3page.pt	(revision 16045)
@@ -0,0 +1,75 @@
+<div i18n:domain="waeup.kofa" i18n:translate=""
+  class="alert alert-success" tal:condition="not: view/getWarnings">
+  Header fields OK
+</div>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Step 3</h3>
+<form i18n:domain="waeup.kofa">
+  <p i18n:translate="">
+    Eventually modify headerfields of import file below.
+  </p>
+  <p>
+    <b i18n:translate="">File:</b>
+    <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename"
+     tal:attributes="value view/filename" />
+  </p>
+  <p>
+    <b i18n:translate="">Processor:</b>
+    <span tal:content="view/importer/name">Importer Name</span>
+    <input type="hidden" name="importer"
+     tal:attributes="value view/importer/util_name" />
+  </p>
+  <p>
+    <b i18n:translate="">Processing mode: </b>
+    <span tal:content="view/mode">mode</span>
+    <input type="hidden" name="mode"
+     tal:attributes="value view/mode" />
+  </p>
+
+  <div>
+    <table tal:define="prev_table view/getPreviewTable;
+                       headers view/headerfields;"
+            class="table">
+      <thead>
+        <tr>
+          <th i18n:translate="">Header</th>
+          <th i18n:translate="">change to</th>
+          <th i18n:translate="">Sample Record</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="row prev_table">
+          <td tal:content="python:headers[row[0]]"> value
+          </td>
+          <td>
+            <select name="headerfield" class="form-control">
+              <option tal:repeat="opt view/getPossibleHeaders"
+                tal:attributes="value opt/value;
+                  selected python: opt['value']==headers[row[0]] and 'selected' or '';"
+                tal:content="opt/title">
+                new_header_name
+              </option>
+            </select>
+          </td>
+          <td tal:repeat="value python:row[1:]"
+              tal:content="value"> value
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <input class="btn btn-default" type="submit" name="cancel"
+    tal:attributes="value view/cancel_button" />
+  <input class="btn btn-default" type="reset" name="reset"
+    tal:attributes="value view/reset_button" />
+  <input class="btn btn-default" type="submit" name="update"
+    tal:attributes="value view/update_button" />
+  <br /><br />
+  <input class="btn btn-danger" type="submit" name="back2"
+    tal:attributes="value view/back_button" />
+  <input class="btn btn-primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed"/>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport4page.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport4page.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterimport4page.pt	(revision 16045)
@@ -0,0 +1,20 @@
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Step 4</h3>
+<p i18n:domain="waeup.kofa" i18n:translate="">
+  Batch processing finished.
+</p>
+<p i18n:domain="waeup.kofa">
+  <b i18n:translate="">File:</b> <span tal:content="view/filename">Filename.csv</span>
+</p>
+<p i18n:domain="waeup.kofa">
+  <b i18n:translate="">Processor:</b>
+  <span tal:content="view/importer/name">Importer Name</span>
+</p>
+<p i18n:domain="waeup.kofa">
+  <b i18n:translate="">Processing mode: </b>
+  <span tal:content="view/mode">mode</span>
+</p>
+<form method="POST" i18n:domain="waeup.kofa">
+  <input class="btn btn-primary" type="submit" name="finish"
+    tal:attributes="value view/back_button" />
+</form>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogspage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogspage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogspage.pt	(revision 16045)
@@ -0,0 +1,34 @@
+<div i18n:domain="waeup.kofa" tal:define="files view/files">
+  <p i18n:translate="" tal:condition="not: files">
+  Currently no log files are available.
+  </p>
+  <table tal:condition="files" class="table">
+    <thead>
+      <tr>
+        <th>
+        </th>
+        <th i18n:translate="">File
+        </th>
+        <th i18n:translate="">Date
+        </th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file files">
+        <td>
+          <form method="POST" action="@@show">
+            <input i18n:translate="" type="submit" class="btn btn-primary"
+                   name="show" tal:attributes="value view/show_button" />
+            <input type="hidden" name="logname"
+                   tal:attributes="value file/name" />
+          </form></td>
+        <td tal:content="file/name">file.name</td>
+        <td tal:content="file/uploaddate">Fri Jan 15 17:44:25 2010</td>
+      </tr>
+    </tbody>
+  </table>
+  <form method="POST">
+    <input i18n:translate="" type="submit" class="btn btn-default" name="back"
+        tal:attributes="value view/back_button"/>
+  </form>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt	(revision 16045)
@@ -0,0 +1,17 @@
+<form method="post" class="form-horizontal">
+  <input type="text" name="query" class="form-control"
+	 tal:attributes="value view/query; placeholder view/placeholder" />
+   <br />
+  <input class="btn btn-primary" type="submit" name="search"
+	 tal:attributes="value view/search_button" />
+  <input class="btn btn-default" type="submit" name="back"
+	 tal:attributes="value view/back_button" />
+  <input type="hidden" name="logname"
+	 tal:attributes="value view/filename" />
+</form>
+<br />
+<div tal:condition="view/result|nothing">
+  <pre tal:content="structure view/result">
+    GREPRESULT
+  </pre>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacentermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacentermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacentermanagepage.pt	(revision 16045)
@@ -0,0 +1,30 @@
+<form  i18n:domain="waeup.kofa" method="POST">
+
+  <br />
+  <div class="form-group">
+    <label i18n:translate="">Storage path</label>
+        <input type="text" name="newpath" class="form-control"
+               tal:attributes="value context/storage" />
+  </div>
+
+  <div class="checkbox">
+    <label>
+      <input type="checkbox" name="move"  checked="checked">
+      Copy existing data to new storage?
+    </label>
+  </div>
+
+  <div class="checkbox">
+    <label>
+      <input type="checkbox" name="overwrite">
+      Overwrite contents in new location?
+    </label>
+  </div>
+
+  <input class="btn btn-primary" type="submit" name="save"
+      tal:attributes="value view/save_button" />
+  <input class="btn btn-default" type="reset" name="reset"
+      tal:attributes="value view/reset_button" />
+  <input class="btn btn-default" type="submit" name="cancel"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenterpage.pt	(revision 16045)
@@ -0,0 +1,41 @@
+<p  i18n:domain="waeup.kofa" i18n:translate="">
+The data center helps you to manage portal data. You can upload CSV
+files here, which will be available for import afterwards.
+</p>
+
+<p i18n:domain="waeup.kofa">
+  <b i18n:translate="">Storage path:</b>
+  <span tal:content="context/storage">/foo/bar</span>
+</p>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+    <table i18n:domain="waeup.kofa" class="kofa-data-table dataTableManageFiles">
+      <thead>
+        <tr>
+          <th></th>
+          <th i18n:translate="">File</th>
+          <th i18n:translate="">Datasets</th>
+          <th i18n:translate="">Upload Date</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="file context/getPendingFiles">
+          <td>
+            <input type="checkbox" name="val_id"
+                         tal:attributes="value file/name" />
+          </td>
+          <td><a tal:attributes="href python: view.url(context) +
+                                 '/download?filename=' + file.name"
+                 tal:content="file/name">FILENAME</a></td>
+          <td tal:content="python: file.lines - 1">100</td>
+          <td tal:content="file/uploaddate">Mar 12, 2008</td>
+        </tr>
+      </tbody>
+    </table>
+    <div tal:condition="view/availableActions">
+      <span tal:repeat="action view/actions" tal:omit-tag="">
+        <input tal:replace="structure action/render"/>
+      </span>
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenteruploadpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenteruploadpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/datacenteruploadpage.pt	(revision 16045)
@@ -0,0 +1,100 @@
+<span i18n:translate="">
+    Before uploading a file, check that your file header corresponds
+    with the header format of the selected processor:
+</span>
+<!-- Processors Modal -->
+<button i18n:translate="" class="btn btn-info btn-xs"
+   data-toggle="modal" data-target="#ProcessorsModal"
+   href="../processorsoverview">
+   View available processors (importers)
+</button>
+<div class="modal fade" id="ProcessorsModal" tabindex="-1"
+     role="dialog" aria-labelledby="ProcessorsModalLabel" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+    </div>
+  </div>
+</div>
+<br /><br />
+<span i18n:translate="">
+    Many importer fields are of type 'Choice' which means only definied keywords
+    are allowed:
+</span>
+<!-- Sources Modal -->
+<button i18n:translate="" class="btn btn-info btn-xs"
+   data-toggle="modal" data-target="#SourcesModal"
+   href="../sourcesoverview">
+   View choices
+</button>
+<div class="modal fade" id="SourcesModal" tabindex="-1"
+     role="dialog" aria-labelledby="SourcesModalLabel" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+    </div>
+  </div>
+</div>
+<br />
+<form method="post" enctype="multipart/form-data">
+<br />
+  <table class="form-table">
+    <tr>
+      <td i18n:translate="">
+        File:
+      </td>
+      <td>
+        <div class="input-group half">
+          <div class="input-group-btn">
+            <div class="btn btn-default btn-file">
+              Select&hellip;
+              <input type="file" name="uploadfile:file" />
+            </div>
+          </div>
+          <input type="text" class="form-control" readonly>
+        </div>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">
+        Processor:
+      </td>
+      <td>
+        <select name="importer" class="form-control half">
+          <option tal:repeat="importer view/getImporters"
+              tal:attributes="value importer/title"
+              tal:content="importer/title"> Some Importer
+          </option>
+        </select>
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">
+        Import Mode:
+      </td>
+      <td>
+        <select name="import_mode" class="form-control half">
+          <option value='Create'>Create</option>
+          <option value='Update'>Update</option>
+          <option value='Remove'>Remove</option>
+        </select>
+      </td>
+    </tr>
+  </table>
+  <br />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+      tal:attributes="value view/upload_button" />
+  <input class="btn btn-default" type="submit" name="CANCEL"
+      tal:attributes="value view/cancel_button"/>
+</form>
+
+
+<br /><br />
+
+<div i18n:translate="">
+    After file upload click the 'Process data' button and proceed up to
+    import step 3. Verify that the data format
+    meets all the import criteria and requirements of the processor.
+</div>
+<br />
+<div i18n:translate="">
+    Import managers will be automatically informed by email after file upload.
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_display_form.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_display_form.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_display_form.pt	(revision 16045)
@@ -0,0 +1,12 @@
+<table class="form-table" i18n:domain="waeup.kofa"
+  tal:define="files provider:files">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Uploaded Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt	(revision 16045)
@@ -0,0 +1,16 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tal:files content="structure provider:files" />
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentmanagepage.pt	(revision 16045)
@@ -0,0 +1,156 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Settings</span></a>
+    </li>
+    <li>
+      <a href="#tab2"  data-toggle="tab" i18n:translate="">Courses</a>
+    </li>
+    <li>
+      <a href="#tab3"  data-toggle="tab" i18n:translate="">Certificates</a>
+    </li>
+    <li>
+      <a href="#tab4"  data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Code
+            </th>
+            <th i18n:translate="">Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getCourses"> <td>
+            <input type="checkbox" name="val_id"
+                   tal:attributes="value entry/name" /> </td> <td>
+            <a href="" tal:attributes="href entry/url"
+               tal:content="entry/name">Name</a> </td>
+            <td tal:content="entry/container/title"> Title </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Code
+            </th>
+            <th i18n:translate="">Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getCertificates"> <td>
+            <input type="checkbox" name="val_id"
+                   tal:attributes="value entry/name" /> </td> <td>
+            <a href="" tal:attributes="href entry/url"
+               tal:content="entry/name">Name</a> </td>
+            <td tal:content="entry/container/title"> Title  </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab4" class="tab-pane"> <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles">
+            <td> <input type="checkbox" name="role_id"
+            tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabfouractions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div> <br /><br />
+
+      <input id="filterbox" class="kofa-filterbox form-control half"
+             placeholder="Enter search text, then select item below."
+             type="text" i18n:translate=""/>
+      <br />
+      <div class="form-inline">
+        <div class="form-group">
+          <select id="filteredselect" name="user"
+                  class="form-control" size=5>
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+              <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select class="form-control" id="local_role" name="local_role">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+            <span tal:repeat="action view/actions" tal:omit-tag="">
+              <input tal:condition="python:action.label in view.tabfouractions2"
+                     tal:replace="structure action/render"/>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/departmentpage.pt	(revision 16045)
@@ -0,0 +1,56 @@
+<ul i18n:domain="waeup.kofa" class="tabs nav nav-tabs" data-tabs="tabs">
+  <li class="active"><a href="#tab1" data-toggle="tab">
+    <span i18n:translate="">Courses</span></a>
+  </li>
+  <li><a href="#tab2" data-toggle="tab">
+    <span i18n:translate="">Certificates</span></a>
+  </li>
+</ul>
+<div i18n:domain="waeup.kofa" class="tab-content">
+  <div id="tab1" class="active tab-pane">
+    <br />
+    <table class="kofa-data-table dataTable">
+      <thead>
+        <tr>
+          <th i18n:translate="">Code</th>
+          <th i18n:translate="">Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCourses">
+          <td>
+            <a href=""
+               tal:attributes="href entry/url"
+               tal:content="entry/name">Name</a>
+          </td>
+          <td tal:content="entry/container/title">
+            Title
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <div id="tab2" class="tab-pane">
+    <br />
+    <table class="kofa-data-table dataTable">
+      <thead>
+        <tr>
+          <th i18n:translate="">Code</th>
+          <th i18n:translate="">Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCertificates">
+          <td>
+            <a href=""
+               tal:attributes="href entry/url"
+               tal:content="entry/name">Name</a>
+          </td>
+          <td tal:content="entry/container/title">
+            Title
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/exception.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/exception.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/exception.pt	(revision 16045)
@@ -0,0 +1,12 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>System Error</title>
+  </head>
+  <body>
+    <h1>An unexpected issue has occurred.</h1>
+    <div>
+      Our technical staff have been automatically notified and will be looking
+      into this with the utmost urgency.
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt	(revision 16045)
@@ -0,0 +1,38 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data">
+
+  <table class="table table-hover">
+    <thead>
+      <tr>
+        <th>&nbsp;</th><th>Code</th><th>Title</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="dep context/values">
+        <td>
+          <input type="checkbox"
+                 name="val_id"
+                 tal:attributes="value dep/code" />
+        </td>
+        <td>
+          <a tal:attributes="href python: view.url(dep)"
+             tal:content="dep/__name__">
+            ID
+          </a>
+        </td>
+        <td>
+          <span tal:content="dep/title">
+            ID
+          </span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:condition="python:action.label in view.taboneactions"
+             tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultymanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultymanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultymanagepage.pt	(revision 16045)
@@ -0,0 +1,129 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST" enctype="multipart/form-data">
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+    <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab2" data-toggle="tab">
+      <span tal:content="view/subunits">CONTENTS
+      </span> </a>
+    </li>
+    <li>
+    <a href="#tab3" data-toggle="tab">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <h3 tal:content="view/subunits">DEPARTMENTS</h3>
+      <table class="table table-hover">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Code
+            </th>
+            <th i18n:translate="">Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="dep context/values"> <td>
+              <input type="checkbox" name="val_id"
+                     tal:attributes="value dep/code" /> </td> <td>
+              <a tal:attributes="href python: view.url(dep)"
+                 tal:content="dep/__name__"> ID</a> </td> <td>
+              <span tal:content="dep/title">ID
+              </span> </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane"> <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles">
+            <td>
+              <input type="checkbox" name="role_id"
+              tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <input id="filterbox" class="kofa-filterbox form-control half"
+             placeholder="Enter search text, then select item below."
+             type="text" i18n:translate=""/>
+      <br />
+      <div class="form-inline">
+        <div class="form-group">
+          <select id="filteredselect" name="user"
+                  class="form-control" size=5>
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+              <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group" tal:condition="view/availableActions">
+          <span tal:repeat="action view/actions" tal:omit-tag="">
+            <input tal:condition="python:action.label in view.tabthreeactions2"
+                   tal:replace="structure action/render"/>
+          </span>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultypage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultypage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/facultypage.pt	(revision 16045)
@@ -0,0 +1,19 @@
+<div i18n:domain="waeup.kofa" i18n:translate="" tal:condition="python: not len(context.keys())">
+There are no subobjects registered yet.
+</div>
+
+<table class="table table-hover" i18n:domain="waeup.kofa">
+  <thead>
+    <tr>
+      <th i18n:translate="">Code</th>
+      <th i18n:translate="">Title</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href python: view.url(value)">
+          <span tal:content="value/code">Name</span></a></td>
+      <td tal:content="value/longtitle">Title</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/filedisplay.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/filedisplay.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/filedisplay.pt	(revision 16045)
@@ -0,0 +1,12 @@
+<tr tal:condition = "viewlet/file_exists">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <a tal:attributes="href python: view.url(context, viewlet.download_name)"
+       tal:content="viewlet/download_filename"
+       target="image">
+      LINK
+    </a>
+  </td>
+</tr>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/fileupload.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/fileupload.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/fileupload.pt	(revision 16045)
@@ -0,0 +1,40 @@
+<tr i18n:domain="waeup.kofa" tal:condition = "viewlet/show_viewlet">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+
+    <tal:file tal:condition="viewlet/file_exists">
+      <a tal:attributes="href python: view.url(context, viewlet.download_name)"
+         tal:content="viewlet/download_filename"
+         target="image">
+        LINK
+      </a>&nbsp;
+      <input type="submit"
+             tal:attributes="id python:('delete_%s' % viewlet.input_name);
+                name python:('delete_%s' % viewlet.input_name);
+                value viewlet/delete_button;
+                onclick python:'return window.confirm(\'%s\')' % view.deletion_warning"
+             class="btn btn-danger btn-xs"/>
+    </tal:file>
+
+		<div class="input-group" tal:condition="not: viewlet/file_exists">
+			<div class="input-group-btn">
+				<div class="btn btn-default btn-file" i18n:translate="">
+          Select new file&hellip;
+          <input i18n:name="input_name"
+                 type="file" tal:attributes="name viewlet/input_name" />
+        </div>
+			</div>
+			<input type="text" class="form-control" readonly>
+      <div class="input-group-btn">
+        <input type="submit"
+               tal:attributes="id python:('upload_%s' % viewlet.input_name);
+                name python:('upload_%s' % viewlet.input_name);
+                value viewlet/upload_button"
+               class="btn btn-primary" />
+        (<span tal:replace="viewlet/max_upload_size">10 kB</span> max.)
+      </div>
+    </div>
+  </td>
+</tr>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/findstudentspage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/findstudentspage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/findstudentspage.pt	(revision 16045)
@@ -0,0 +1,53 @@
+<form method="POST" i18n:domain="waeup.kofa" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="student_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="matric_number">with matriculation number</option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+</form>
+
+<p>&nbsp;</p>
+<div tal:condition="view/hitlist">
+  <h3 i18n:translate="">Search Results</h3>
+  <input type="hidden" name="old_searchterm"
+       tal:attributes="value view/searchterm" />
+  <input type="hidden" name="old_searchtype"
+       tal:attributes="value view/searchtype" />
+  <table class="kofa-data-table dataTable">
+    <thead>
+    <tr>
+      <th i18n:translate="">Student Id</th>
+      <th i18n:translate="">Reg. Number</th>
+      <th i18n:translate="">Matric. Number</th>
+      <th i18n:translate="">State</th>
+      <th i18n:translate="">Full Name</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr tal:repeat="item view/hitlist">
+      <td> <a tal:attributes="href item/url">
+        <span tal:content="item/student_id">A123456</span></a>
+      </td>
+      <td class="center"> <a tal:attributes="href item/url">
+        <span tal:content="item/reg_number">9999999</span></a>
+      </td>
+      <td class="center"> <a tal:attributes="href item/url">
+        <span tal:content="item/matric_number">9999999</span></a>
+      </td>
+      <td tal:content="item/state">admitted</td>
+      <td tal:content="item/display_fullname">Bob</td>
+    </tr>
+    </tbody>
+  </table>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/jobindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/jobindex.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/jobindex.pt	(revision 16045)
@@ -0,0 +1,21 @@
+<html>
+  <head>
+  </head>
+  <body>
+    <div>
+      Job: <span tal:content="python:view.context.__repr__()">JOB</span>
+    </div>
+    <div>
+      Status: <span tal:content="view/context/status">status</span>
+    </div>
+    <div>
+      Begin after: <span tal:content="view/context/begin_after">after</span>
+    </div>
+    <div>
+      Begin by: <span tal:content="view/context/begin_by">by</span>
+    </div>
+    <div>
+      Percent done: <span tal:content="view/percent">percent</span>
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/loginpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/loginpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/loginpage.pt	(revision 16045)
@@ -0,0 +1,40 @@
+<form i18n:domain="waeup.kofa" method="post" class="form-horizontal">
+  <br />
+  <div class="form-group">
+    <label i18n:translate="" class="col-sm-2 control-label">User Name or Id</label>
+    <div class="col-sm-10">
+      <input name="form.login" type="text" class="form-control" id="login">
+    </div>
+  </div>
+  <div class="form-group">
+    <label i18n:translate="" class="col-sm-2 control-label">Password</label>
+    <div class="col-sm-10">
+      <input name="form.password" type="password" class="form-control" id="password">
+      <font  i18n:translate="" color="red"> Notice: User names, Ids and passwords are case sensitive.</font>
+    </div>
+  </div>
+  <input type="hidden" name="camefrom"   tal:attributes="value view/camefrom" />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+  <br /><br /><br />
+  <p  i18n:translate="login_trouble1">Don't forget to logout or exit your browser when you're done.
+    If you are having trouble logging in, make sure to enable  cookies in your web browser.
+  </p>
+  <p i18n:translate="login_trouble2" tal:condition="python:False">
+    You don't have an account because you are a fresh student, or your student record has just been created?
+    Acquire a Password Activation Code (PWD) and inititialize your student account
+    <strong><a href ="setpassword"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble4">
+    You don't have an account because you are a fresh student, or your student record has just been created?
+    Inititialize your student account <strong><a href="requestpw"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble3"> Or simply forgot your student id, applicant id or password? Then request a new password
+    <strong><a href="changepw"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble4"> You are student parents and want to monitor study progress
+    of your child? Then request a temporary parents password <strong><a href="requestppw"> here</a></strong>.
+    Prerequisite for getting access is that you have a valid email address and that this address
+    has been entered by the student into the parents' email address field.
+  </p>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/logoutpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/logoutpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/logoutpage.pt	(revision 16045)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/messages.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/messages.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/messages.pt	(revision 16045)
@@ -0,0 +1,6 @@
+<div
+  tal:repeat="message view/messages"
+  tal:attributes="class message/type"
+  tal:content="structure message/message">Sauron will arrive soon!
+</div>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mycourses.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mycourses.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mycourses.pt	(revision 16045)
@@ -0,0 +1,13 @@
+<h3 i18n:domain="waeup.kofa" i18n:translate="">
+  Academic Session: <span tal:content="view/academic_session">SESSION</span>
+</h3>
+
+<div tal:repeat="course view/getCourses">
+    <a tal:attributes="href python:view.url(course)">
+      <span tal:content="python:course.__parent__.__parent__.__parent__.code">FAC</span> /
+      <span tal:content="python:course.__parent__.__parent__.code">DEP</span> /
+      <span tal:content="course/longtitle">COURSE</span>
+    </a>
+</div>
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/myrolespage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/myrolespage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/myrolespage.pt	(revision 16045)
@@ -0,0 +1,29 @@
+<h3 i18n:domain="waeup.kofa"
+    i18n:translate="">My Portal Roles:</h3>
+<table i18n:domain="waeup.kofa" class="form-table">
+<tr tal:repeat="role view/getSiteRoles">
+    <td>
+      <div tal:content="role" >ROLE</div>
+    </td>
+  </tr>
+</table>
+
+<h3 i18n:domain="waeup.kofa"
+    i18n:translate="">My Local Roles:</h3>
+<table class="form-table">
+  <tr tal:repeat="role view/getLocalRoles">
+    <td valign="top" >
+      <span tal:content="role">ROLE</span>:</td>
+    <td>
+      <span tal:repeat="object python:view.getLocalRoles[role]">
+      <a tal:attributes="href python:view.url(object)"
+         tal:content="python:view.url(object)" >OBJECT
+      </a>
+      <br />
+      </span>
+    </td>
+  </tr>
+</table>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mytranscriptrequests.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mytranscriptrequests.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/mytranscriptrequests.pt	(revision 16045)
@@ -0,0 +1,61 @@
+<h3 i18n:domain="waeup.kofa" i18n:translate="" tal:condition="python:view.getStudents[0]">
+  Transcripts to be validated
+</h3>
+
+<table class="kofa-data-table dataTable" tal:condition="python:view.getStudents[0]">
+  <thead>
+  <tr>
+    <th i18n:translate="">Matric No.</th>
+    <th i18n:translate="">Student Id</th>
+    <th i18n:translate="">Fullname</th>
+    <th i18n:translate="">Faculty</th>
+    <th i18n:translate="">Department</th>
+    <th i18n:translate="">Course of Studies</th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr tal:repeat="student python:view.getStudents[0]">
+    <td tal:content="student/context/matric_number">MATRICNUMBER</td>
+    <td>
+      <a tal:attributes="href python:student.url + '/studycourse/transcript'">
+      <span tal:content="student/student_id">STUDENTID</span>
+      </a>
+    </td>
+    <td tal:content="student/context/display_fullname">FULLNAME</td>
+    <td tal:content="student/context/faccode">FACCODE</td>
+    <td tal:content="student/context/depcode">DEPCODE</td>
+    <td tal:content="student/context/certcode">CERTCODE</td>
+  </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="" tal:condition="python:view.getStudents[1]">
+  Transcripts to be signed and released
+</h3>
+
+<table class="kofa-data-table dataTable" tal:condition="python:view.getStudents[1]">
+  <thead>
+  <tr>
+    <th i18n:translate="">Matric No.</th>
+    <th i18n:translate="">Student Id</th>
+    <th i18n:translate="">Fullname</th>
+    <th i18n:translate="">Faculty</th>
+    <th i18n:translate="">Department</th>
+    <th i18n:translate="">Course of Studies</th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr tal:repeat="student python:view.getStudents[1]">
+    <td tal:content="student/context/matric_number">MATRICNUMBER</td>
+    <td>
+      <a tal:attributes="href python:student.url + '/studycourse/transcript'">
+      <span tal:content="student/student_id">STUDENTID</span>
+      </a>
+    </td>
+    <td tal:content="student/context/display_fullname">FULLNAME</td>
+    <td tal:content="student/context/faccode">FACCODE</td>
+    <td tal:content="student/context/depcode">DEPCODE</td>
+    <td tal:content="student/context/certcode">CERTCODE</td>
+  </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/notfound.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/notfound.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/notfound.pt	(revision 16045)
@@ -0,0 +1,16 @@
+<h2 i18n:domain="waeup.kofa" i18n:translate="">
+  The page you are trying to access is not available.
+</h2>
+
+<div i18n:domain="waeup.kofa">
+  <b i18n:translate="">Please note the following:</b></div>
+<div i18n:domain="waeup.kofa">
+  <ol class="list">
+    <li i18n:translate="">
+      You might have misspelled the URL.
+    </li>
+    <li i18n:translate="">
+        You might be trying to access a non-existing page.
+    </li>
+  </ol>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/plainactionbutton.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/plainactionbutton.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/plainactionbutton.pt	(revision 16045)
@@ -0,0 +1,7 @@
+<span tal:condition="viewlet/target_url">
+  <span>
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/primarynavtab.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/primarynavtab.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/primarynavtab.pt	(revision 16045)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active" tal:condition="viewlet/link_target">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/processorsoverview.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/processorsoverview.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/processorsoverview.pt	(revision 16045)
@@ -0,0 +1,89 @@
+<div class="modal-body">
+  <div class="panel-group"
+       id="accordion"
+       role="tablist"
+       aria-multiselectable="true"
+       i18n:domain="waeup.kofa"
+       >
+    <div class="panel panel-default"  tal:repeat="importer view/getImporters">
+      <div class="panel-heading" role="tab"
+           tal:attributes="id python: 'heading' + importer['name']">
+        <h4 class="panel-title">
+          <a class="collapsed" data-toggle="collapse"
+             data-parent="#accordion"
+             tal:attributes="href python: '#collapse' + importer['name'];
+                             aria-controls python: 'collapse' + importer['name']"
+             aria-expanded="false">
+            <span tal:replace="importer/title">TITLE</span>
+          </a>
+        </h4>
+      </div>
+      <div tal:attributes="id python: 'collapse' + importer['name'];
+                           aria-labelledby python: 'heading' + importer['name']"
+           class="panel-collapse collapse"
+           role="tabpanel">
+        <div class="panel-body">
+          <table class="table table-condensed">
+            <thead>
+              <tr>
+                <th i18n:translate="">Processor</th>
+                <th i18n:translate="">Required Schema Fields</th>
+                <th i18n:translate="">Optional Schema Fields</th>
+                <th i18n:translate="">Non-Schema Fields</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td>
+                  <span tal:content="importer/title">TITLE</span><br /><br />
+                  <a i18n:translate="" class="btn btn-primary btn-xs"
+                     tal:attributes="href python: view.url(context)
+                                          + '/skeleton?name='
+                                          + importer['name']">
+                     Download CSV File Template
+                  </a>
+                </td>
+                <td nowrap>
+                  <span tal:repeat="field importer/fields">
+                      <tal:schemafield condition="field/f_type">
+                        <tal:required condition="field/f_required">
+                          <span tal:replace="field/f_name"></span>
+                          (<span tal:replace="field/f_type"></span>)
+                          <br />
+                        </tal:required>
+                      </tal:schemafield>
+                  </span>
+                </td>
+                <td nowrap>
+                  <span tal:repeat="field importer/fields">
+                      <tal:schemafield condition="field/f_type">
+                        <tal:notrequired condition="not:field/f_required">
+                          <span tal:replace="field/f_name"></span>
+                          (<span tal:replace="field/f_type"></span>)
+                          <br />
+                        </tal:notrequired>
+                      </tal:schemafield>
+                  </span>
+                </td>
+                <td>
+                  <span tal:repeat="field importer/fields">
+                      <tal:extrafield condition="not:field/f_type">
+                        <span tal:replace="field/f_name"></span>
+                        <br />
+                      </tal:extrafield>
+                  </span>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+  <br />
+  <button type="button" class="btn btn-default" style="float: right"
+          i18n:translate="" data-dismiss="modal"
+          tal:condition="python: view.__name__ == 'processorsoverview'">
+      Close
+  </button>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainercreate.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 16045)
@@ -0,0 +1,28 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    Here you can create reports representing portal data.  Please pick
+    the type of report you want to create from the selection below.
+    Afterwards you can configure the report (do the settings).  The
+    report data will be calculated and then made available for you to
+    view/download.
+  </p>
+  <br />
+  <div class="form-group">
+    <label for="generator">Report Type</label>
+    <select name="generator" class="form-control half">
+      <span tal:repeat="items view/creators" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  </div>
+  <div class="form-group">
+    <input i18n:translate="" type="submit" class="btn btn-primary"
+           name="START_GENERATOR" value="Configure" />
+    <a class="btn btn-default" href=""
+       tal:attributes="href python:view.url(context)">Cancel</a>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 16045)
@@ -0,0 +1,64 @@
+<table i18n:domain="waeup.kofa" class="table">
+  <thead>
+    <tr>
+      <th i18n:translate="">Report Number</th>
+      <th i18n:translate="">Description</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="entry view/entries">
+      <td>
+        <span tal:replace="python: entry[0]">12</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[1]">REPORT DESCRIPTION</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[7]">CREATOR</span>
+      </td>
+      <td nowrap>
+        <span tal:replace="python: entry[6]">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="python: entry[2]">STATUS</span>
+      </td>
+      <td nowrap>
+        <form method="POST">
+          <input type="hidden" name="job_id"
+                 tal:attributes="value python: entry[0]" />
+          <input type="submit" class="btn btn-sm btn-primary"
+                 name="DOWNLOAD" value="Download"
+                 tal:condition="python: entry[4]" />
+          <input type="submit" class="btn btn-sm btn-default"
+                 name="DISCARD" value="Discard"
+                 tal:condition="python: entry[3]" />
+          <a href="" class="btn btn-sm btn-default" i18n:translate=""
+             tal:condition="python: entry[5]">
+            <img src="/static/img/actionicon_reload.png" />
+            Reload
+          </a>
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<form method="POST" i18n:domain="waeup.kofa">
+  <div class="form-group">
+    <a tal:attributes="href python:view.url(context, 'create')"
+       class="btn btn-primary" i18n:translate="">
+      Create new report
+    </a>
+    <input type="submit" class="btn btn-default"
+      name="PURGE" value="Purge outdated reports"
+      data-toggle="tooltip" title="All reports older than 4 weeks will be discarded!"
+      onclick="return window.confirm('Are you really sure?')"/>
+  </div>
+</form>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/searchpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/searchpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/searchpage.pt	(revision 16045)
@@ -0,0 +1,46 @@
+<form method="POST" class="form-inline">
+<br />
+  <div class="form-group">
+      <input type="text" name="query" class="form-control"
+    	 tal:attributes="value view/query" />
+  </div>
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="SUBMIT"
+      tal:attributes="value view/search_button"/>
+  </div>
+</form>
+<br />
+<span  i18n:domain="waeup.kofa" tal:condition="view/hitlist">
+    <h3 i18n:translate="">Search Results</h3>
+    <table class="kofa-data-table dataTable">
+        <thead>
+            <tr>
+                <th i18n:translate="">Code</th>
+                <th i18n:translate="">Title</th>
+                <th i18n:translate="">Faculty</th>
+                <th i18n:translate="">Department</th>
+                <th i18n:translate="">Type</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr tal:repeat="item view/hitlist"  class="gradeB">
+            <td>
+                <a href="" tal:attributes="href item/url"
+                           tal:content="item/code">CODE</a>
+            </td>
+            <td tal:content="item/title">
+                TITLE
+            </td>
+            <td tal:content="item/fac">
+                FACULTY
+            </td>
+            <td tal:content="item/dep">
+                DEPARTMENT
+            </td>
+            <td tal:content="item/type">
+                TTYPE
+            </td>
+            </tr>
+        </tbody>
+    </table>
+</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/source.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/source.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/source.pt	(revision 16045)
@@ -0,0 +1,35 @@
+<div class="panel panel-default">
+  <div class="panel-heading" role="tab"
+       tal:attributes="id viewlet/heading">
+    <h4 class="panel-title">
+      <a class="collapsed" data-toggle="collapse"
+         tal:attributes="data-parent viewlet/accordion;
+                         href viewlet/hash_collapse;
+                         aria-controls viewlet/collapse"
+         aria-expanded="false">
+        <span tal:replace="viewlet/title">TITLE</span>
+      </a>
+    </h4>
+  </div>
+  <div tal:attributes="id viewlet/collapse;
+                       aria-labelledby viewlet/heading"
+       class="panel-collapse collapse"
+       role="tabpanel">
+    <div class="panel-body">
+      <table class="table">
+        <thead>
+          <tr>
+            <th i18n:translate="">Value</th>
+            <th i18n:translate="">Title</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="item viewlet/source">
+            <td tal:content="python: item[0]">NAME</td>
+            <td tal:content="python: item[1]">TITLE</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/sourcesoverview.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/sourcesoverview.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/sourcesoverview.pt	(revision 16045)
@@ -0,0 +1,24 @@
+<div class="modal-body" i18n:domain="waeup.kofa">
+  <table width="100%">
+    <tr>
+      <td valign="top">
+        <div class="panel-group" id="accordion-left" role="tablist"
+             aria-multiselectable="true">
+          <tal:sourcesleft tal:content="structure provider:sources_left" />
+        </div>
+      </td>
+      <td valign="top">
+        <div class="panel-group" id="accordion-right" role="tablist"
+             aria-multiselectable="true">
+          <tal:sourcesright tal:content="structure provider:sources_right" />
+        </div>
+      </td>
+    </tr>
+  </table>
+
+  <button type="button" class="btn btn-default" style="float: right"
+          i18n:translate="" data-dismiss="modal"
+          tal:condition="python: view.__name__ == 'sourcesoverview'">
+      Close
+  </button>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/staffsitelayout.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/staffsitelayout.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/staffsitelayout.pt	(revision 16045)
@@ -0,0 +1,159 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" i18n:domain="waeup.kofa">
+  <head>
+
+    <!-- Diazo element-->
+    <title i18n:translate="">
+      WAeUP.Kofa - Student Management System
+    </title>  
+    <!-- /Diazo element-->
+
+    <!--base href="http://localhost:8080/app/@@page"
+     tal:attributes="href python: view.url(layout.site)" / -->
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <meta name="robots" content="index, follow" />
+    <link rel="alternate" type="application/rss+xml" title="RSS"
+        href="http://www.example.net/feed.rss"
+        tal:attributes="title string:RSS feed of ${layout/getAppTitle};
+            href python: view.url(layout.site, 'feed.rss')" />
+  </head>
+
+  <!-- Diazo element-->
+  <body class="kofa-body">
+
+    <!-- Diazo element-->
+    <a class="kofa-brand-link" href="#"
+        tal:attributes="href python: view.url(layout.site)"
+        tal:content="layout/getAppAcronym">ACRONYM
+    </a>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav kofa-navbar-left"> 
+      <tal:primary_nav content="structure provider:primary_nav" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav secondary-nav kofa-navbar-right">
+      <li tal:condition="not: layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+          tal:attributes="href python: view.url(layout.site, 'login')">
+          Login
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+         Contact
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(context, '@@logout')">
+         Logout
+      </a>
+      </li>
+    </ul>
+    <!-- /Diazo element-->
+
+    <span tal:define="langs provider:languages"
+          tal:condition="langs"
+          class="language">
+      <tal:languages tal:content="structure langs" /> |
+    </span>
+
+    <!-- Diazo element-->
+    <span tal:condition="layout/isMaintenanceMode">
+      <div class="maintenance-warning" i18n:translate="">
+          M A I N T E N A N C E &nbsp;&nbsp;&nbsp;
+          M O D E &nbsp;&nbsp;&nbsp; O N
+      </div>
+    </span>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="breadcrumb">
+      <tal:breadcrumbs
+            tal:condition="python: layout.isAuthenticated()"
+            tal:content="structure provider:breadcrumbs" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <span tal:condition="layout/getStudentName">
+      <div class="wfstatus">
+            <span tal:replace="python:context.student.student_id">
+              Id
+            </span>
+            &bull;
+            <span tal:replace="python:context.student.translated_state">
+              State
+            </span>
+      </div>
+    </span>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="sidebar well">
+        <div tal:condition="layout/getStudentName">
+          <div class="kofa-sidebar-title" tal:content="layout/getStudentName">
+            STUDENTNAME
+          </div>
+          <ul class="nav">
+            <tal:left content="structure provider:left_studentmanage" />
+          </ul>
+        </div>
+        <div tal:condition="layout/isAuthenticated">
+          <div class="kofa-sidebar-title" tal:content="layout/getUserTitle">
+            USERNAME
+          </div>
+          <ul class="nav">
+            <tal:left content="structure provider:left_manage" />
+          </ul>
+      </div>
+    </div>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="kofa-content">
+      <div class="actionbar" tal:condition="provider:actionbar">
+        <tal:actionbar content="structure provider:actionbar" />&nbsp;
+      </div>
+      <div
+          tal:define="message context/@@messages"
+          tal:condition="python: len(message)>11">
+        <div tal:content="structure message">
+        </div>
+      </div>
+      <div class="alert alert-danger"
+        tal:define="status view/status|nothing"
+        tal:condition="status">
+        <span tal:content="view/status">
+          FORM STATUS SUMMARY
+        </span>:
+        <ul id="alert-error" tal:condition="view/errors">
+          <li tal:repeat="error view/error_views">
+             <span tal:replace="structure error">Error Type</span>
+          </li>
+        </ul>
+      </div>
+      <h1 class="kofa-content-label"
+          tal:condition="view/label|nothing"
+          tal:content="structure view/label">LABEL</h1>
+      <div tal:content="structure view/content"> THE CONTENT
+      </div>
+      <br />
+      <div class="documentation" tal:condition="view/doclink|nothing">
+        <a href="#" tal:attributes="href view/doclink"
+           i18n:translate="" target="_blank">
+          Kofa Docs for this page
+        </a>
+      </div>
+    </div>
+    <!-- /Diazo element-->
+
+  </body>
+  <!-- /Diazo element-->
+
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/studentsitelayout.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/studentsitelayout.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/studentsitelayout.pt	(revision 16045)
@@ -0,0 +1,122 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" i18n:domain="waeup.kofa">
+  <head>
+    <title i18n:translate="">
+      WAeUP.Kofa - Student Management System
+    </title>
+    <!--base href="http://localhost:8080/app/@@page"
+     tal:attributes="href python: view.url(layout.site)" / -->
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    <meta name="robots" content="index, follow" />
+    <link rel="stylesheet" media="only screen and (max-device-width: 480px)"
+        tal:attributes="href python: view.url(layout.site, '@@/waeup.kofa.browser/mobile.css')"
+        type="text/css" />
+    <link rel="alternate" type="application/rss+xml" title="RSS"
+        href="http://www.example.net/feed.rss"
+        tal:attributes="title string:RSS feed of ${layout/getAppTitle};
+            href python: view.url(layout.site, 'feed.rss')" />
+  </head>
+
+  <!-- Diazo element-->
+  <body class="kofa-body">
+
+    <!-- Diazo element-->
+    <a class="kofa-brand-link" href="#"
+        tal:attributes="href python: view.url(layout.site)"
+        tal:content="layout/getAppAcronym">ACRONYM</a>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav kofa-navbar-left">
+        <tal:primary_nav content="structure provider:primary_nav" />
+        <tal:primary_nav content="structure provider:primary_nav_student" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <ul class="nav secondary-nav kofa-navbar-right">
+      <li tal:condition="not: layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, 'login')">
+          Login
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+          Contact
+      </a>
+      </li>
+      <li tal:condition="layout/isAuthenticated">
+      <a href="#" i18n:translate=""
+         tal:attributes="href python: view.url(context, '@@logout')">
+         Logout
+      </a>
+      </li>
+    </ul>
+    <!-- Diazo element-->
+
+    <span tal:define="langs provider:languages"
+          tal:condition="langs"
+          class="language">
+      <tal:languages tal:content="structure langs" /> |
+    </span>
+
+    <!-- Diazo element-->
+    <ul class="breadcrumb" tal:condition="layout/isAuthenticated">
+      <tal:breadcrumbs
+            tal:condition="python: layout.isAuthenticated()"
+            tal:content="structure provider:breadcrumbs" />
+    </ul>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <span tal:condition="layout/getStudentName">
+      <div class="wfstatus">
+            <span tal:replace="python:context.student.student_id">
+              Id
+            </span>
+            &bull;
+            <span tal:replace="python:context.student.translated_state">
+              State
+            </span>
+      </div>
+    </span>
+    <!-- /Diazo element-->
+
+    <!-- Diazo element-->
+    <div class="kofa-content">
+      <div class="actionbar" tal:condition="provider:actionbar">
+        <tal:actionbar content="structure provider:actionbar" />&nbsp;
+      </div>
+      <div tal:define="message context/@@messages"
+           tal:condition="python: len(message)>11">
+        <div tal:content="structure message">
+        </div>
+      </div>
+      <div class="alert alert-danger"
+        tal:define="status view/status|nothing"
+        tal:condition="status">
+        <span tal:content="view/status">
+          FORM STATUS SUMMARY
+        </span>:
+        <ul id="alert-error" tal:condition="view/errors">
+          <li tal:repeat="error view/error_views">
+             <span tal:replace="structure error">Error Type</span>
+          </li>
+        </ul>
+      </div>
+      <h1 class="kofa-content-label"
+          tal:condition="view/label|nothing"
+          tal:content="structure view/label">Label</h1>
+      <div tal:content="structure view/content">
+        THE CONTENT
+      </div>
+      <br />
+    </div>
+    <!-- /Diazo element-->
+
+  </body>
+  <!-- /Diazo element-->
+
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/subobjectlist.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/subobjectlist.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/subobjectlist.pt	(revision 16045)
@@ -0,0 +1,15 @@
+<table>
+  <thead>
+    <tr tal:repeat="viewlet viewletmanager/viewlets">
+      <th tal:replace="structure viewlet/render">viewlet
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="row viewletmanager/rows">
+      <td tal:repeat="viewlet row">
+        <div tal:replace="structure viewlet/render">viewlet
+        </div></td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/unauthorized.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/unauthorized.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/unauthorized.pt	(revision 16045)
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>Not Authorized</title>
+  </head>
+  <body>
+    <div class="warning">
+      <b>You are not authorized to access this resource.</b>
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universitypage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universitypage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universitypage.pt	(revision 16045)
@@ -0,0 +1,1 @@
+<span tal:replace="structure view/frontpage">Frontpage</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universityrss20feed.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universityrss20feed.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/universityrss20feed.pt	(revision 16045)
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0"
+     xmlns:tal="http://xml.zope.org/namespaces/tal"
+     xmlns:atom="http://www.w3.org/2005/Atom">
+  <channel>
+    <title>
+      <tal:block tal:content="view/title"/>
+      <tal:block
+	  tal:define="title view/contexttitle"
+	  tal:condition="title"> - <tal:block content="title"/>
+      </tal:block>
+    </title>
+    <link
+	tal:content="view/link"></link>
+    <description
+	tal:content="view/description"></description>
+    <language
+	tal:condition="view/language"
+	tal:content="view/language"></language>
+    <pubDate
+	tal:condition="view/date"
+	tal:content="view/date"></pubDate>
+    <lastBuildDate
+	tal:condition="view/buildDate"
+	tal:content="view/buildDate"></lastBuildDate>
+    <managingEditor
+	tal:condition="view/editor"
+	tal:content="view/editor"></managingEditor>
+    <webMaster
+	tal:condition="view/webmaster"
+	tal:content="view/webmaster"></webMaster>
+    <atom:link
+	tal:attributes="href string:${view/link}@@feeds/${view/name}"
+	rel="self" type="application/rss+xml" />
+    <item
+	tal:repeat="item view/entries">
+      <title
+	  tal:content="item/title"></title>
+      <description
+	  tal:content="item/description"></description>
+      <guid
+	  tal:content="item/guid"
+	  tal:define="isPermaLink item/isPermaLink|nothing"
+	  tal:attributes="isPermaLink python:isPermaLink and 'true' or 'false'"></guid>
+      <link
+	  tal:condition="item/link|nothing"
+	  tal:content="item/link"></link>
+      <author
+	  tal:condition="item/author|nothing"
+	  tal:content="item/author"></author>
+      <category
+	  tal:condition="item/category|nothing"
+	  tal:content="item/category"></category>
+      <pubDate
+	  tal:condition="item/pubDate|nothing"
+	  tal:content="item/pubDate"></pubDate>
+    </item>
+  </channel>
+</rss>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/usereditformpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 16045)
@@ -0,0 +1,35 @@
+<p class="alert alert-danger"  i18n:domain="waeup.kofa"
+   i18n:translate="officer_suspended_warning"
+   tal:condition="python: getattr(context, 'suspended', False)">
+  <strong>ATTENTION:</strong>
+  This account has been suspended. The officer can't login.
+</p>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+      <tr>
+        <td i18n:translate="" class="fieldname">Password:</td>
+        <td>
+          <input class="form-control" name="password" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">Retype Password:</td>
+        <td>
+          <input class="form-control" name="control_password" type="password" />
+        </td>
+      </tr>
+      <tal:files content="structure provider:files" />
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/userscontainerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 16045)
@@ -0,0 +1,32 @@
+<table i18n:domain="waeup.kofa" class="kofa-data-table dataTable">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Name</th>
+      <th i18n:translate="">Portal Roles</th>
+      <th i18n:translate="">Local Roles</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="account context/values">
+      <td tal:content="account/name">USERNAME</td>
+      <td>
+        <span tal:content="account/title">TITLE</span>
+        <span style="color:red" tal:condition="account/suspended">(suspended)</span>
+      </td>
+      <td nowrap tal:content="structure python:view.getSiteRoles(account)">SITE ROLES</td>
+      <td tal:content="structure python:view.getLocalRoles(account)">LOCAL ROLES</td>
+      <td class="text-right">
+        <form method="post">
+          <input type="hidden" name="userid"
+           tal:attributes="value account/name"/>
+            <input class="btn btn-sm btn-primary" type="submit" name="manage"
+              tal:attributes="value view/manage_button" />
+            <input class="btn btn-sm btn-default" type="submit" name="delete"
+              tal:attributes="value view/delete_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 16045)
@@ -0,0 +1,45 @@
+<h1>Running and Completed Jobs</h1>
+<div>
+  <form method="POST">
+    <input type="submit" class="btn primary" name="RELOAD" value="reload" />
+    <input type="submit" class="btn secondary"
+	   name="START_NEW" value="start new test job" />
+  </form>
+</div>
+<hr />
+
+<div tal:repeat="job view/jobs">
+  <div>
+    JOB
+    <a href="" tal:attributes="href job/link">
+      <span tal:replace="job/job_id">12</span>
+    </a>
+    status: <span tal:replace="job/status">new</span>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin by:</span>
+      <span tal:content="job/begin_by"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">begin after:</span>
+      <span tal:content="job/begin_after"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">annotations:</span>
+      <span tal:content="job/annotations"></span>
+    </div>
+    <div>
+      &nbsp;&nbsp;
+      <span class="span3">failure:</span>
+      <span tal:content="job/failure"></span>
+    </div>
+  </div>
+  <form tal:condition="job/removable" method="POST">
+    <input type="submit" class="btn secondary" name="REMOVE" value="Remove" />
+    <input type="hidden" name="job_id"
+	   tal:attributes="value job/job_id" />
+  </form>
+  <hr />
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/widgets.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/widgets.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/templates/widgets.pt	(revision 16045)
@@ -0,0 +1,26 @@
+<tal:block repeat="widget view/widgets">
+  <tr tal:define="separators view/separators|nothing"
+      tal:condition="python: separators and separators.get(widget.name,None)">
+    <td class="separator" colspan=2 tal:content="python: separators[widget.name]">
+      HEADLINE
+    </td>
+  </tr>
+  <tr>
+    <td class="fieldname">
+      <span tal:condition="widget/required">*</span>
+      <span tal:content="widget/label">LABEL</span>:
+    </td>
+    <td>
+      <span tal:content="structure widget">
+        <input type="text" />
+      </span>
+      <tal:error tal:condition="widget/error">
+        <div tal:content="structure widget/error">ERROR</div>
+      </tal:error>
+      <tal:hint
+        tal:condition="python: widget.hint and not getattr(view,'hide_hint',False)">
+        <div class="hint" tal:content="structure widget/hint">HINT</div>
+      </tal:hint>
+    </td>
+  </tr>
+</tal:block>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file.csv	(revision 16045)
@@ -0,0 +1,3 @@
+File with forbidden chracters
+
+This is character 160:  
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file_2.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file_2.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/forbidden_file_2.csv	(revision 16045)
@@ -0,0 +1,2 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,state
+X666666,Aaren ,Pieri,1,1990-01-02,100000,,1234,M,courses validated
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_async.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_async.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_async.py	(revision 16045)
@@ -0,0 +1,189 @@
+import os
+import shutil
+import tempfile
+import transaction
+from zc.async.testing import wait_for_result
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from waeup.kofa.app import University
+from waeup.kofa.async import AsyncJob, get_job_id
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.testing import FunctionalLayer, expensive_actions
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+
+
+def fake_get_export_jobs_status(user_id=None):
+    return [('completed', 'Completed', 'my_exporter'), ]
+
+
+def dummy_func():
+    return 42
+
+
+class AsyncBrowserTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AsyncBrowserTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        app = University()
+        app['datacenter'].setStoragePath(self.storage)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+        self.browser = Browser()
+        self.browser.handleErrors = False
+        self.datacenter_path = 'http://localhost/app/datacenter'
+
+    def tearDown(self):
+        super(AsyncBrowserTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+
+
+class JobViewTests(AsyncBrowserTests):
+    # Tests for the view of single jobs
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobViewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that starts a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_get_job_view(self):
+        # we can get a job on a view
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.open(self.overview_path)
+        job_id = get_job_id(job)
+        link = self.browser.getLink(job_id)
+        expected_url = 'http://localhost/app/jobs/%s' % job_id
+        self.assertEqual(link.url, expected_url)
+        link.click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # we need some permissions to get a job view
+        transaction.commit()  # make sure the 'app' is stored in ZODB
+        job = self.start_job()
+        wait_for_result(job)
+        job_id = get_job_id(job)
+        link_url = 'http://localhost/app/jobs/%s' % job_id
+
+        # get a new browser, where we're not authenticated
+        browser = Browser()
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, link_url)
+        return
+
+
+class JobOverviewTests(AsyncBrowserTests):
+    # Tests for the job overview page of universities
+    overview_path = 'http://localhost/app/jobs'
+
+    def setUp(self):
+        super(JobOverviewTests, self).setUp()
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+
+    def start_job(self):
+        # helper that start a job in background and returns the job
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        manager.put(job, site=self.getRootFolder())
+        return job
+
+    def test_job_high_load(self):
+        # under high load, jobs won't start (None is returned then)
+        manager = getUtility(IJobManager)
+        job = AsyncJob(dummy_func)
+        with expensive_actions() as utils:
+            utils.expensive_actions_allowed = lambda: False
+            result = manager.put(job, site=self.getRootFolder())
+        self.assertTrue(result is None)
+
+    def test_job_get(self):
+        # we can get the overview page
+        self.browser.open(self.overview_path)
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_anonymous_forbidden(self):
+        # anonymous user will be kept out
+        browser = Browser()  # get a browser w/o credentials set
+        browser.handleErrors = False
+        self.assertRaises(Unauthorized, browser.open, self.overview_path)
+        return
+
+    def test_job_reload(self):
+        # we can reload the overview page
+        self.browser.open(self.overview_path)
+        self.browser.getControl('reload').click()
+        self.assertTrue("Running and Completed Jobs" in self.browser.contents)
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        return
+
+    def test_jobs_appear(self):
+        # any job running will appear
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('status: Completed' in self.browser.contents)
+        return
+
+    def test_job_removal(self):
+        # we can remove jobs via the overview page
+        self.browser.open(self.overview_path)
+        job = self.start_job()
+        wait_for_result(job)
+        self.browser.getControl('reload').click()
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 1)
+        self.browser.getControl('Remove').click()
+        self.assertEqual(self.browser.headers['status'], '200 Ok')
+        self.assertTrue('Removed job' in self.browser.contents)
+        self.assertEqual(len(list(getUtility(IJobManager).jobs())), 0)
+        return
+
+
+class DataCenterJSONTests(AsyncBrowserTests):
+
+    def test_status_plain(self):
+        # if no job was triggered, no need to update something.
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/json')
+        self.assertEqual(self.browser.contents,
+                         '{"reload": false, "interval": 0, "html": ""}')
+        return
+
+    def test_status_completed(self):
+        # if all jobs are completed, we should reload the page
+        self.app[
+            'datacenter'].get_export_jobs_status = fake_get_export_jobs_status
+        path = '%s/status' % self.datacenter_path
+        self.browser.open(path)
+        self.assertEqual(self.browser.contents,
+                         '{"reload": true, "interval": 0, "html": ""}')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_browser.py	(revision 16045)
@@ -0,0 +1,669 @@
+# -*- coding: utf-8 -*-
+## $Id$
+## 
+## Copyright (C) 2011-2013 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
+##
+"""
+Extends the doctests in browser.txt and datacenter.txt.
+"""
+import shutil
+import tempfile
+import os
+import grok
+from datetime import datetime, timedelta
+from zc.async.testing import wait_for_result
+from zope.component import createObject, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IJobManager, IUserAccount, IKofaUtils
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.tests.test_authentication import SECRET
+
+SAMPLE_FILE = os.path.join(os.path.dirname(__file__), 'test_file.csv')
+FORBIDDEN_FILE = os.path.join(os.path.dirname(__file__), 'forbidden_file.csv')
+FORBIDDEN_FILE_2 = os.path.join(
+    os.path.dirname(__file__), 'forbidden_file_2.csv')
+
+class UniversitySetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UniversitySetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = u'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.study_mode = 'ug_ft'
+        self.certificate.start_level = 100
+        self.certificate.end_level = 500
+        self.certificate.school_fee_1 = 40000.0
+        self.certificate.school_fee_2 = 20000.0
+        self.app['faculties']['fac1'] = Faculty(code='fac1')
+        self.app['faculties']['fac1']['dep1'] = Department(code=u'dep1')
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        self.course = createObject('waeup.Course')
+        self.course.code = 'COURSE1'
+        self.course.semester = 1
+        self.course.credits = 10
+        self.course.passmark = 40
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
+            self.course, level=100)
+
+        # Configure university and hostels
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2004
+        configuration.clearance_fee = 3456.0
+        configuration.booking_fee = 123.4
+        self.app['configuration'].addSessionConfiguration(configuration)
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+        self.datacenter_path = 'http://localhost/app/datacenter'
+
+    def tearDown(self):
+        super(UniversitySetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+class DataCenterUITests(UniversitySetup):
+    # Tests for DataCenter class views and pages.
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.datacenter_path)
+        return
+
+    def test_logs_listed(self):
+        # existing logfiles are listed in logs overview
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        log_path = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'myspecial.log')
+        with open(log_path, "a") as fd:
+            fd.write("SOME FOO IN LOGFILE")
+        self.browser.open(self.datacenter_path + "/logs")
+        assert "myspecial.log" in self.browser.contents
+
+    def test_view_log(self):
+        # single logfiles can be watched (with filter regexp)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        log_path = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'myspecial.log')
+        with open(log_path, "a") as fd:
+            fd.write("SOME FOO IN LOGFILE")
+        self.browser.open(
+            self.datacenter_path + "/@@show/?logname=myspecial.log&query=.*")
+        assert "SOME FOO IN LOGFILE" in self.browser.contents
+
+    def test_html_in_logs_escaped(self):
+        # HTML code in logfiles is escaped before output.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        log_path = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'myspecial.log')
+        with open(log_path, "a") as fd:
+            fd.write("SOME <TAG> AND ÜMLÄUTS IN LOG")
+        self.browser.open(
+            self.datacenter_path + "/@@show/?logname=myspecial.log&query=.*")
+        assert "SOME &lt;TAG&gt; AND ÜMLÄUTS IN LOG" in self.browser.contents
+
+    def test_file_download_delete(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        file = open(SAMPLE_FILE)
+        ctrl = self.browser.getControl(name='uploadfile:file')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file, filename='my_test_data.csv')
+        self.browser.getControl('Upload').click()
+        self.browser.open(self.datacenter_path)
+        self.browser.getLink('my_test_data_zope.mgr.csv').click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.FileDownloadView - '
+                        'downloaded: my_test_data_zope.mgr.csv' in logcontent)
+        self.browser.open(self.datacenter_path)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='my_test_data_zope.mgr.csv').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully deleted: my_test_data_zope.mgr.csv'
+            in self.browser.contents)
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.DatacenterPage - '
+                        'deleted: my_test_data_zope.mgr.csv' in logcontent)
+        return
+
+    def test_forbidden_file_upload(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        file = open(FORBIDDEN_FILE)
+        ctrl = self.browser.getControl(name='uploadfile:file')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file, filename='my_corrupted_file.csv')
+        self.browser.getControl('Upload').click()
+        self.assertTrue(
+            'Your file contains forbidden characters or'
+            in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.DatacenterUploadPage - '
+            'invalid file uploaded:' in logcontent)
+        return
+
+    def test_forbidden_file_upload_2(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        file = open(FORBIDDEN_FILE_2)
+        ctrl = self.browser.getControl(name='uploadfile:file')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file, filename='my_corrupted_file.csv')
+        self.browser.getControl('Upload').click()
+        self.assertTrue(
+            'The data in your file contain trailing whitespaces.'
+            in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.DatacenterUploadPage - '
+            'invalid file uploaded:' in logcontent)
+        return
+
+class DataCenterUIExportTests(UniversitySetup, FunctionalAsyncTestCase):
+    # Tests for DataCenter class views and pages.
+
+    layer = FunctionalLayer
+
+    def wait_for_export_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_datacenter(self, job_id):
+        # tell whether job_id is stored in datacenter's running jobs list
+        for entry in list(self.app['datacenter'].running_exports):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_export(self):
+        # helper to start an export. Make sure to remove the result file
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.browser.getLink("Export data").click()
+        self.browser.getControl(name="exporter").value = ['faculties']
+        self.browser.getControl("Create CSV file").click()
+        return
+
+    def cleanup_run_job(self):
+        # helper to remove any result file of an export
+        job_id = self.wait_for_export_job_completed()
+        manager = getUtility(IJobManager)
+        job = manager.get(job_id)
+        if os.path.exists(job.result):
+            shutil.rmtree(os.path.dirname(job.result))
+
+    def test_export_start(self):
+        # we can trigger export file creation
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Export data").click()
+        self.browser.getControl(name="exporter").value = ['faculties']
+        self.browser.getControl("Create CSV file").click()
+        # we can trigger export file creation only once
+        self.browser.getControl(name="exporter").value = ['departments']
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Sorry, another export job is running.'
+                         in self.browser.contents)
+        self.wait_for_export_job_completed()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        self.browser.getControl(name="exporter").value = ['departments']
+        self.browser.getControl("Create CSV file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(len(self.app['datacenter'].running_exports), 2)
+        # The second job is running
+        self.assertTrue('<img src="/static/img/actionicon_reload.png" />'
+                         in self.browser.contents)
+        self.cleanup_run_job() # make sure to remove temp dirs
+        # Also the second job must be cleaned up
+        manager = getUtility(IJobManager)
+        job_id = self.app['datacenter'].running_exports[1][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        if os.path.exists(job.result):
+            shutil.rmtree(os.path.dirname(job.result))
+        return
+
+    def test_export_disabled(self):
+        self.app['configuration'].export_disabled_message = u'Export forbidden'
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Export data").click()
+        self.browser.getControl(name="exporter").value = ['faculties']
+        self.browser.getControl("Create CSV file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('div class="alert alert-danger">Export forbidden</div>'
+                         in self.browser.contents)
+        self.browser.getLink("selecting certain export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['all']
+        self.browser.getControl(name="level").value = ['all']
+        self.browser.getControl(name="mode").value = ['all']
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('div class="alert alert-danger">Export forbidden</div>'
+                         in self.browser.contents)
+        return
+
+    def test_export_download(self):
+        # we can download a generated export result
+        self.trigger_export()
+        # while the export file is created, we get a reload button
+        # (or a loading bar if javascript is enabled)...
+        self.assertTrue('Reload' in self.browser.contents)
+        # ...which is displayed as long as the job is not finished.
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/export')
+        self.assertFalse('Reload' in self.browser.contents)
+        # ...we can download the result
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.headers['content-disposition'],
+            'attachment; filename="WAeUP.Kofa_faculties_%s.csv' % job_id)
+        self.assertEqual(self.browser.contents,
+            'code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2\r\n'
+            'fac1,Unnamed Faculty,faculty,[],,\r\n')
+        # Thew job can be discarded
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+
+        # after download, the job and the result file are removed
+        #manager = getUtility(IJobManager)
+        #result = manager.get(job_id)
+        #self.assertEqual(result, None)
+        #self.assertEqual(self.stored_in_datacenter(job_id), False)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVPage - exported: '
+                        'faculties, job_id=' in logcontent)
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
+                        'downloaded: WAeUP.Kofa_faculties_%s.csv, job_id=%s'
+                        % (job_id, job_id) in logcontent)
+        return
+
+    def test_export_accesscodes(self):
+        # Create portal manager and an ExportManager
+        self.app['users'].addUser('mrportal', SECRET)
+        self.app['users']['mrportal'].email = 'mrportal@foo.ng'
+        self.app['users']['mrportal'].title = 'Carlo Pitter'
+        # Assign PortalManager role
+        prmlocal = IPrincipalRoleManager(self.app)
+        prmlocal.assignRoleToPrincipal('waeup.PortalManager', 'mrportal')
+        self.app['users'].addUser('mrexporter', SECRET)
+        self.app['users']['mrexporter'].email = 'mrexporter@foo.ng'
+        self.app['users']['mrexporter'].title = 'Carlos Potter'
+        # Assign ExportManager role
+        prmlocal.assignRoleToPrincipal('waeup.ExportManager', 'mrexporter')
+
+        # Login as portal manager
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'mrportal'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+
+        self.browser.open(self.datacenter_path + '/export')
+        self.assertTrue(
+            '<option value="accesscodebatches">' in self.browser.contents)
+        self.assertTrue('<option value="accesscodes">' in self.browser.contents)
+        self.browser.getControl(name="exporter").value = ['accesscodes']
+        self.browser.getControl("Create CSV file").click()
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/@@export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(
+            'batch_num,batch_prefix,batch_serial,cost,history,owner,'
+            'random_num,representation,state\r\n',
+            self.browser.contents)
+
+        # Thew job can be discarded
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+
+        # Login as officer who is not allowed to download accesscodes
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'mrexporter'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+
+        self.browser.open(self.datacenter_path + '/export')
+        # The Export Manager can see the accesscodebatches exporter ...
+        self.assertTrue(
+            '<option value="accesscodebatches">' in self.browser.contents)
+        # ... but not the accesscodes exporter.
+        self.assertFalse('<option value="accesscodes">'
+                         in self.browser.contents)
+
+    def test_export_students(self):
+        # we can trigger export file creation
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Export data").click()
+        self.browser.getLink("selecting certain export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['all']
+        self.browser.getControl(name="level").value = ['all']
+        self.browser.getControl(name="mode").value = ['all']
+        self.browser.getControl("Create CSV file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+          'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.headers['content-disposition'],
+          'attachment; filename="WAeUP.Kofa_students_%s.csv' % job_id)
+        # No students yet
+        self.assertEqual(self.browser.contents,
+            'adm_code,clr_code,date_of_birth,email,'
+            'employer,firstname,flash_notice,lastname,matric_number,middlename,'
+            'nationality,officer_comment,parents_email,'
+            'perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'password,state,history,certcode,is_postgrad,current_level,'
+            'current_session\r\n')
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
+            '- exported: students '
+            '(None, None, None, None, None, None, , , all, all, all, all, all), '
+            'job_id=%s'
+            % job_id in logcontent)
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVView '
+            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent)
+        self.cleanup_run_job()
+        return
+
+    def test_export_discard(self):
+        # we can discard a generated export result
+        self.trigger_export()
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertTrue('Discarded export' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage - discarded: job_id=%s'
+            % job_id in logcontent)
+        return
+
+    def test_export_purge(self):
+        self.trigger_export()
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(self.datacenter_path + '/export')
+        self.browser.getControl("Purge").click()
+        # Flash message always says successful ...
+        self.assertTrue(
+            'Exports successfully purged.' in self.browser.contents)
+        # ... but the export is still there
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+
+        job_id, gen_name, user = self.app['datacenter'].get_running_export_jobs()[0]
+        job = getUtility(IJobManager).get(job_id)
+        tz = getUtility(IKofaUtils).tzinfo
+        delta = timedelta(days=5)
+        setattr(job, '_begin_after', datetime.now(tz) - delta)
+        self.browser.getControl("Purge").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - browser.pages.ExportCSVPage - '
+                        'purged: job_id=%s' % job_id in logcontent)
+        return
+
+    def test_user_data_import_permission(self):
+        upload_path = 'http://localhost/app/datacenter/upload'
+        login_path = 'http://localhost/app/login'
+        # Create  officer
+        self.app['users'].addUser('officer', SECRET)
+        self.app['users']['officer'].email = 'mrofficer@foo.ng'
+        self.app['users']['officer'].title = 'Carlo Pitter'
+        # Assign role
+        prm = IPrincipalRoleManager(self.app)
+        prm.assignRoleToPrincipal('waeup.ImportManager', 'officer')
+        # Login
+        self.browser.open(login_path)
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.browser.open(upload_path)
+        self.assertFalse('User Processor' in self.browser.contents)
+        # Assign role which is necessary to import user data
+        prm.assignRoleToPrincipal('waeup.UsersManager', 'officer')
+        self.browser.open(upload_path)
+        self.assertTrue('User Processor' in self.browser.contents)
+        return
+
+class SupplementaryBrowserTests(UniversitySetup):
+    # These are additional tests to browser.txt
+
+    def test_set_former_course(self):
+        # A certificate course will be automatically removed when the
+        # former_course attribute of the corresponding course is set.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.assertTrue('COURSE1_100' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        self.browser.open(
+            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1/manage')
+        self.browser.getControl(name="form.former_course").value = ['selected']
+        self.browser.getControl("Save").click()
+        self.assertFalse('COURSE1_100' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        return
+
+    def test_remove_course(self):
+        # We add a second certificate course which refers
+        # to the same course but at a different level.
+        self.app['faculties']['fac1']['dep1'].certificates[
+            'CERT1'].addCertCourse(
+            self.course, level=210)
+        # Both certificate courses will be automatically removed when
+        # the corresponding course is removed.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.assertTrue('COURSE1_100' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        self.assertTrue('COURSE1_210' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        self.browser.open('http://localhost/app/faculties/fac1/dep1/manage')
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='COURSE1').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertFalse('COURSE1_100' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        self.assertFalse('COURSE1_210' in self.app[
+            'faculties']['fac1']['dep1'].certificates['CERT1'])
+        return
+
+    def test_suspended_officer(self):
+        self.app['users'].addUser(
+            'officer', SECRET, title='Bob Officer', email='bob@abcd.ng')
+        # Officer can't login if their password is not set.
+        self.app['users']['officer'].password = None
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        # We set the password again.
+        IUserAccount(
+            self.app['users']['officer']).setPassword(SECRET)
+        # Officers can't login if their account is suspended/deactivated.
+        self.app['users']['officer'].suspended = True
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...but your account has been temporarily deactivated...',
+            self.browser.contents)
+        # Officer is really not logged in.
+        self.assertFalse("Bob Officer" in self.browser.contents)
+        self.app['users']['officer'].suspended = False
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        # Yeah, officer logged in.
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        self.assertTrue("Bob Officer" in self.browser.contents)
+        self.browser.getLink("Logout").click()
+        # Login as manager.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # Suspended accounts are marked in red.
+        self.browser.open('http://localhost/app/users')
+        self.assertFalse('(suspended)' in self.browser.contents)
+        self.app['users']['officer'].suspended = True
+        self.browser.open('http://localhost/app/users')
+        self.assertTrue(
+            '<span style="color:red">(suspended)</span>'
+            in self.browser.contents)
+        # A flash message appears on the edit page of the officer.
+        self.browser.open('http://localhost/app/users/officer')
+        self.assertTrue(
+            'This account has been suspended.' in self.browser.contents)
+        self.app['users']['officer'].suspended = False
+        self.browser.open('http://localhost/app/users/officer')
+        self.assertFalse(
+            'This account has been suspended.' in self.browser.contents)
+        return
+
+    def test_maintenance_mode(self):
+        config = grok.getSite()['configuration']
+        self.app['users'].addUser(
+            'officer', SECRET, title='Bob Officer', email='bob@abcd.ng')
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        # Officer logged in.
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.assertTrue("Bob Officer" in self.browser.contents)
+        # If maintenance mode is enabled, officer is immediately logged out.
+        config.maintmode_enabled_by = u'any_user'
+        self.assertRaises(
+            Unauthorized, self.browser.open, 'http://localhost/app/faculties')
+        self.browser.open('http://localhost/app/login')
+        self.assertMatches(
+            '...The portal is in maintenance mode...',
+            self.browser.contents)
+        # Officers really can't login if maintenance mode is enabled.
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        # A second warning is raised.
+        self.assertMatches(
+            '...The portal is in maintenance mode. You can\'t login!...',
+            self.browser.contents)
+        # Offficer can login if s/he is the blocker.
+        config.maintmode_enabled_by = u'officer'
+        self.browser.getControl(name="form.login").value = 'officer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertTrue('You logged in' in self.browser.contents)
+        self.assertTrue('Logout' in self.browser.contents)
+        return
+
+    def test_sources_overview(self):
+        self.browser.open('http://localhost/app/sources')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('id="headingStudentStates"' in self.browser.contents)
+
+    def test_processors_overview(self):
+        self.browser.open('http://localhost/app/processors')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink(url='skeleton?name=facultyprocessor').click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.contents,
+            'code,local_roles,officer_1,officer_2,title,title_prefix\r\n')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_captcha.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_captcha.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_captcha.py	(revision 16045)
@@ -0,0 +1,284 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## 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
+##
+import grok
+from zope.component import getAdapter, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface import verify
+from zope.publisher.browser import TestRequest
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.browser.captcha import (
+    CaptchaResponse, CaptchaRequest, NullCaptcha, StaticCaptcha, ReCaptcha,
+    CaptchaManager)
+from waeup.kofa.browser.interfaces import (
+    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
+    ICaptchaManager)
+
+
+class CaptchaResponseTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = CaptchaResponse(True)
+        verify.verifyClass(ICaptchaResponse, CaptchaResponse)
+        verify.verifyObject(ICaptchaResponse, obj)
+        return
+
+    def test_init_vals(self):
+        # make sure the initial values are stored
+        resp = CaptchaResponse(False, 'some-msg')
+        self.assertEqual(resp.is_valid, False)
+        self.assertEqual(resp.error_code, 'some-msg')
+        return
+
+class CaptchaRequestTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfces
+        obj = CaptchaRequest(None)
+        verify.verifyClass(ICaptchaRequest, CaptchaRequest)
+        verify.verifyObject(ICaptchaRequest, obj)
+        return
+
+    def test_init_vals(self):
+        # make sure initial values are stored
+        req = CaptchaRequest('my-solution', 'my-challenge')
+        self.assertEqual(req.solution, 'my-solution')
+        self.assertEqual(req.challenge, 'my-challenge')
+        return
+
+class CaptchaTestBase(object):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = self.factory()
+        verify.verifyClass(ICaptcha, self.factory)
+        verify.verifyObject(ICaptcha, obj)
+        return
+
+    def test_utility(self):
+        # the null captcha is also registered as a utility for ICaptcha
+        captcha = getUtility(ICaptcha, name=self.name)
+        self.assertTrue(isinstance(captcha, self.factory))
+        return
+
+class NullCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = NullCaptcha
+    name = 'No captcha'
+
+    def test_verify(self):
+        # null captchas accept any input request
+        captcha = self.factory()
+        result1 = captcha.verify(TestRequest())
+        result2 = captcha.verify(
+            TestRequest(form={'solution': 'a', 'challenge': 'b'}))
+        self.assertEqual(result1.is_valid, True)
+        self.assertEqual(result2.is_valid, True)
+        return
+
+    def test_display(self):
+        # null captchas do not generate additional HTML code
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertEqual(result, u'')
+        return
+
+class StaticCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = StaticCaptcha
+    name = 'Testing captcha'
+
+    def test_verify(self):
+        # id captchas accept any solution that matches challenge and
+        # is not empty or None
+        captcha = self.factory()
+        request1 = TestRequest(form={'solution': 'my-sol',
+                                     'challenge': 'my-challenge'})
+        request2 = TestRequest()
+        request3 = TestRequest(form={'solution': '', 'challenge': ''})
+        request4 = TestRequest(form={'solution': 'my-sol',
+                                     'challenge': 'my-sol'})
+        result1 = captcha.verify(request1)
+        result2 = captcha.verify(request2)
+        result3 = captcha.verify(request3)
+        result4 = captcha.verify(request4)
+        self.assertEqual(result1.is_valid, False)
+        self.assertEqual(result2.is_valid, False)
+        self.assertEqual(result3.is_valid, False)
+        self.assertEqual(result4.is_valid, True)
+        return
+
+    def test_display(self):
+        # id captchas provide a simple HTML input
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertMatches(
+            '<input type="hidden" name="challenge"'
+            '      value="..." /><br />Type: ...<br />'
+            '<input type="text" name="solution" value="the-solution" /><br />',
+            result)
+        return
+
+class ReCaptchaTests(FunctionalTestCase, CaptchaTestBase):
+
+    layer = FunctionalLayer
+
+    factory = ReCaptcha
+    name = 'ReCaptcha'
+
+    def DISABLEDtest_verify(self):
+        # recaptcha verification cannot be tested easily. As the
+        # solution and other environment parameters is only known to
+        # the remote server, we cannot guess on server side what the
+        # solution is.  Further more this test contacts a remote
+        # server so that we might want to disable this test by
+        # default.
+        captcha = self.factory()
+        request1 = TestRequest(
+            form={'recaptcha_challenge_field': 'my-challenge',
+                  'recaptcha_response_field': 'my-solution'})
+        result1 = captcha.verify(request1)
+        self.assertEqual(result1.is_valid, False)
+        self.assertEqual(result1.error_code, 'invalid-request-cookie')
+        return
+
+    def test_display(self):
+        # recaptchas provide the pieces to trigger the remote API
+        captcha = self.factory()
+        result = captcha.display()
+        self.assertMatches(
+            '<script type="text/javascript" '
+            'src="..." async defer></script>'
+            '<div class="g-recaptcha" data-sitekey="..."></div>',
+            result)
+        return
+
+class CaptchaManagerTests(FunctionalTestCase):
+    # These tests do not require a site setup
+    # See testsuite below for insite tests.
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = CaptchaManager()
+        verify.verifyClass(ICaptchaManager, CaptchaManager)
+        verify.verifyObject(ICaptchaManager, obj)
+        return
+
+    def test_utility(self):
+        # the global captcha chooser is registered as a global utility
+        result = getUtility(ICaptchaManager)
+        self.assertTrue(isinstance(result, CaptchaManager))
+        return
+
+    def test_get_avail_captchas(self):
+        # we can get a dict of available captchas.
+        chooser = getUtility(ICaptchaManager)
+        result = dict(chooser.getAvailCaptchas())
+        no_captcha = result.get('No captcha', None)
+        self.assertTrue(no_captcha is not None)
+        default_captcha = result.get(u'', None)
+        self.assertTrue(default_captcha is None)
+        return
+
+    def test_get_captcha_wo_site(self):
+        # if there is no site set, we get the default captcha
+        setSite(None)
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+class FakeSite(grok.Site, grok.Container):
+    pass
+
+class FakeConfiguration(object):
+    captcha = None
+
+    def __init__(self, captcha=None):
+        self.captcha = captcha
+
+class CaptchaManagerTestsWithSite(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CaptchaManagerTestsWithSite, self).setUp()
+        self.getRootFolder()['app'] = FakeSite()
+        self.site = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(CaptchaManagerTestsWithSite, self).tearDown()
+        clearSite(self.site)
+        return
+
+    def test_get_captcha_no_captcha_in_config(self):
+        # if a site has no configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_empty_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = dict()
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_invalid_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = FakeConfiguration(captcha='invalid name')
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is default)
+        return
+
+    def test_get_captcha_invalid_captcha_in_config(self):
+        # if a site has None as configuration setting for captchas, we get
+        # the default captcha
+        setSite(self.site)
+        self.site['configuration'] = FakeConfiguration(captcha='No captcha')
+        chooser = getUtility(ICaptchaManager)
+        result = chooser.getCaptcha()
+        no_captcha = getUtility(ICaptcha, name='No captcha')
+        default = getUtility(ICaptcha)
+        self.assertTrue(result is not default)
+        self.assertTrue(result is no_captcha)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_doctests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_doctests.py	(revision 16045)
@@ -0,0 +1,28 @@
+## $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
+##
+"""Setup doctests for browser module.
+"""
+from waeup.kofa.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite([
+        'doctests/batchprocessing_browser.txt',
+        'doctests/breadcrumbs.txt',
+        'doctests/pages.txt',
+        ])
+    return suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_file.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_file.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_file.csv	(revision 16045)
@@ -0,0 +1,1 @@
+empty file
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_pdf.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_pdf.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_pdf.py	(revision 16045)
@@ -0,0 +1,541 @@
+import os
+import tempfile
+import unittest
+from reportlab.lib import colors
+from reportlab.platypus.flowables import PageBreak
+from waeup.kofa.browser.pdf import (
+    format_html, normalize_signature, format_signatures,
+    vert_signature_cell, horiz_signature_cell, signature_row, get_sig_tables,
+    PDFCreator, Paragraph, Table, NORMAL_STYLE, sig_table,
+    get_signature_tables)
+
+DRAW_SAMPLE_GRID = True
+
+class HelperTest(unittest.TestCase):
+
+    def test_format_html(self):
+        # test format_html()
+        intext = 'Two\nLines'
+        self.assertEqual(format_html(intext), 'Two<br />Lines')
+        intext = '<div>Two</div>\nLines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        intext = '<div>Two</div>Lines'
+        self.assertEqual(format_html(intext), '<div>Two</div><br />Lines')
+        return
+
+class SignatureNormalizerTests(unittest.TestCase):
+    # Tests for normalize_signature()
+
+    def test_invalid_input_type(self):
+        # we accept only strings and tuples
+        self.assertRaises(
+            # we don't accept number, for instance
+            ValueError, normalize_signature, 12)
+        return
+
+    def test_invalid_number_of_items(self):
+        # a signature tuple must contain 1, 2, or 3 elements
+        self.assertRaises(
+            # empty tuple
+            ValueError, normalize_signature, ())
+        self.assertRaises(
+            # overcrowded tuple
+            ValueError, normalize_signature, ('a', 'b', 'c', 'd'))
+        return
+
+    def test_simple_string(self):
+        # we accept a simple string
+        self.assertEqual(
+            normalize_signature('sig1'),
+            ((None, 'sig1', None)))
+        # also unicode is allowed
+        self.assertEqual(
+            normalize_signature(u'sig1'),
+            ((None, u'sig1', None)))
+
+    def test_one_item_tuple(self):
+        # we accept a tuple with a single signature string
+        self.assertEqual(
+            normalize_signature(('sig1',)),
+            ((None, 'sig1', None)))
+        return
+
+    def test_two_items_tuple(self):
+        # we accept a tuple with two items
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1')),
+            (('pre-text', 'sig1', None)))
+        return
+
+    def test_three_items_tuple(self):
+        # we accept a tuple with three items, of course
+        self.assertEqual(
+            normalize_signature(('pre-text', 'sig1', 'post-text')),
+            (('pre-text', 'sig1', 'post-text')))
+        return
+
+class SignaturesFormatterTests(unittest.TestCase):
+
+    def test_single_signature(self):
+        # we can format a single signature (w/o pre or post blocks)
+        data, style = format_signatures(['My Signature'])[0]
+        self.assertEqual(
+            data, [['Date', '', 'My Signature', '']])
+        #self.assertEqual(style, )
+        return
+
+    def test_single_sig_with_pre_text(self):
+        # we can format a single signature (with pre-text)
+        data, style = format_signatures([('Pre-Text', 'My Sig', None)])[0]
+        self.assertEqual(
+            data,
+            [['Pre-Text', '', '', ''],
+             ['Date', '', 'My Sig', '']])
+        return
+
+    def test_single_sig_with_post_text(self):
+        # we can format a single signature (with post-text)
+        data, style = format_signatures([(None, 'My Sig', 'Post-Text')])[0]
+        self.assertEqual(
+            data,
+            [['Date', '', 'My Sig', ''],
+             ['Post-Text', '', '', '']])
+        return
+
+    def test_single_sig_with_pre_and_post_text(self):
+        # we can format a single signature (with pre- and post-text)
+        data, style = format_signatures([('Pre', 'My Sig', 'Post')])[0]
+        self.assertEqual(
+            data,
+            [['Pre', '', '', ''],
+             ['Date', '', 'My Sig', ''],
+             ['Post', '', '', '']])
+        return
+
+class SignatureCellFormatterTests(unittest.TestCase):
+
+    def test_vert_signature(self):
+        # we can create vertical signatures
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['Post']])
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_custom_row_and_col(self):
+        # custom row and col nums are respected
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=5, start_col=3)
+        self.assertEqual(
+            style,
+            (('LINEABOVE', (3, 7), (3, 7), 1, colors.black),)
+            )
+        return
+
+    def test_vert_signature_no_underline(self):
+        # we can suppress the underlining of signature cell
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), underline=False)
+        self.assertEqual(
+            style, ())
+        return
+
+    def test_vert_signature_custom_date(self):
+        # we can create vertical signatures with customized date text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data,
+            [['Pre'], ['My Date'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_date(self):
+        # we can create vertical signatures without date
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data,
+            [['Pre'], [''], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_pre(self):
+        # we can create vertical signatures w/o pre-text
+        data, style, cols = vert_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data,
+            [[''], ['Date:'], ['Sig'], ['Post']])
+        return
+
+    def test_vert_signature_no_post(self):
+        # we can create vertical signatures w/o post-text
+        data, style, cols = vert_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data,
+            [['Pre'], ['Date:'], ['Sig'], ['']])
+        return
+
+    def test_horiz_signature_cell(self):
+        # we can create horizontal signatures
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+             ))
+        return
+
+    def test_horiz_signature_cell_no_date(self):
+        # we can create horizontal signatures without date
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_field=False)
+        self.assertEqual(
+            data, [['Pre'], [''], ['Sig'], ['Post']])
+        self.assertEqual(
+            style, (('LINEABOVE', (0, 2), (0, 2), 1, colors.black),))
+        return
+
+    def test_horiz_signature_cell_custom_date(self):
+        # we can create horizontal signatures with custom date text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), date_text='My Date')
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['My Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_pre(self):
+        # we can create horizontal signatures without pre-text
+        data, style, cols = horiz_signature_cell(
+            (None, 'Sig', 'Post'))
+        self.assertEqual(
+            data, [['', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['Post', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_no_post(self):
+        # we can create horizontal signatures without post-text
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', None))
+        self.assertEqual(
+            data, [['Pre', '', ''],
+                   ['', '', ''],
+                   ['Date', '', 'Sig'],
+                   ['', '', '']])
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (0, 0), (2, 0)),
+                ('SPAN', (0, 3), (2, 3)),
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ))
+        return
+
+    def test_horiz_signature_cell_custom_row_and_col(self):
+        # custom col and row numbers are respected in styles
+        data, style, cols = horiz_signature_cell(
+            ('Pre', 'Sig', 'Post'), start_row=3, start_col=5)
+        self.assertEqual(
+            style,
+            (
+                ('SPAN', (5, 3), (7, 3)),
+                ('SPAN', (5, 6), (7, 6)),
+                ('LINEABOVE', (5, 5), (5, 5), 1, colors.black),
+                ('LINEABOVE', (7, 5), (7, 5), 1, colors.black),
+                ))
+        return
+
+class SignatureRowFormatterTests(unittest.TestCase):
+
+    def test_vertical_row(self):
+        data, style, cols = signature_row(
+            ['Sig 1', 'Sig 2',])
+        self.assertEqual(
+            data, [['', '', ''], ['Date:', '', 'Date:'],
+                   ['Sig 1', '', 'Sig 2'], ['', '', '']]
+            )
+        self.assertEqual(
+            style, (
+                ('LINEABOVE', (0, 2), (0, 2), 1, colors.black),
+                ('LINEABOVE', (2, 2), (2, 2), 1, colors.black),
+                ('TOPPADDING', (0, 2), (-1, 2), 0),
+                ('BOTTOMPADDING', (0, 1), (-1, 1), 36),
+                ('LEFTPADDING', (0, 0), (-1, 0), 1),
+                ('LEFTPADDING', (0, 3), (-1, 3), 1),
+                ('TOPPADDING', (0, 0), (-1, 0), -6),
+                ('BOTTOMPADDING', (0, 0), (-1, 0), -6),
+                ('TOPPADDING', (0, 3), (-1, 3), -6),
+                ('BOTTOMPADDING', (0, 3), (-1, 3), -6)
+                )
+            )
+        return
+
+def samples_dir():
+    # create a directory for sample sheets, if it doesn't exist,
+    # and return its path.
+    tmp_dir = os.path.join(
+        tempfile.gettempdir(), 'waeup.kofa.pdf-samples')
+    if not os.path.exists(tmp_dir):
+        os.mkdir(tmp_dir)
+    return tmp_dir
+
+def draw_sample_pdf(data, name='signatures.pdf',
+                    title='Sample Title', author='Test Author'):
+    # create a pdf document with data as content
+    #data = [Paragraph('<b>Sample</b>', NORMAL_STYLE)]
+    creator = PDFCreator()
+    pdf = creator.create_pdf(
+        data, title=title, author=author)
+    path = os.path.join(samples_dir(), name)
+    open(path, 'wb').write(pdf)
+    return path
+
+
+class SampleSheet(unittest.TestCase):
+
+    def add_tables(self, data, title, tables, grid=DRAW_SAMPLE_GRID):
+        # add tables to `data`
+        mod_tables = []
+        data += [Paragraph(title, NORMAL_STYLE),]
+        for table in tables:
+            if grid:
+                table.setStyle((
+                    ('INNERGRID',(0,0),(-1,-1), 0.25, colors.blue),
+                    ('BOX', (0,0), (-1, -1), 0.25, colors.blue)),)
+            data += [table,]
+        return data
+
+
+    def test_draw_sample(self):
+        # create a sample sheet as PDF
+        # single signature
+        data = [
+            Paragraph('Signature Samples in here are created by '
+                      'calling the '
+                      '<font face="Times">get_signature_tables()</font> '
+                      'function.<br />'
+                      'Grids are drawn for better visibility of '
+                      'internal table structure only. You can set '
+                      'DRAW_SAMPLE_GRID value in browser/tests/test_pdf.py.'
+                      '<br /><br />',
+                      NORMAL_STYLE),
+            ]
+
+        tables = get_signature_tables(['Signature 1'])
+        data = self.add_tables(
+            data, '<b>Single Signature:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"])</font>',
+            tables)
+
+        # two signatures in a row
+        tables = get_signature_tables(['Signature 1', 'Signature 2'])
+        data = self.add_tables(
+            data, '<br /><b>Two Signatures:</b> '
+            '<font face="Times">get_signature_tables(["Signature 1",'
+            ' "Signature 2"])</font></b>',
+            tables)
+
+        # single signature with pre/post text
+        tables = get_signature_tables(
+            [('Pre-Text', 'Signature 1', 'Post-Text')])
+        data = self.add_tables(
+            data,
+            '<br /><b>Single Signature with Pre- and Post Text:</b> '
+            '<font face="Times">get_signature_tables('
+            '[("Pre-Text", "Signature 1", "Post-Text")])</font>',
+            tables)
+
+        # three signatures
+        tables = get_signature_tables(['Sig 1', 'Sig 2', 'Sig 3'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Three Signatures:</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3"])</font>',
+            tables)
+
+        # four signatures
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (each row a table)</b>',
+            tables)
+
+        # four sigs in one table
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], single_table=True)
+        data = self.add_tables(
+            data,
+            '<br /><b>Sample: Four Signatures (as one table)</b>',
+            tables)
+
+
+        data += [PageBreak(), Paragraph(
+            '<br /><br /><font size="14"><font face="Times">horizontal</font>'
+            ' Parameter (with one sig): </b><br />',
+            NORMAL_STYLE)]
+
+        # one signature (default behaviour)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=None)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (default, horizontal=None):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=None)</font></b>',
+            tables)
+
+        # one signature (horizontal)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=True)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (horizontal=True):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=True)</font></b>',
+            tables)
+
+        # one signature (vertical)
+        tables = get_signature_tables(
+            ['Sig 1'], horizontal=False)
+        data = self.add_tables(
+            data, '<br /><b>One Signature (horizontal=False):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1"], '
+            'horizontal=False)</font></b>',
+            tables)
+
+        data += [Paragraph(
+            '<br /><br /><font size="14"><font face="Times">horizontal</font>'
+            ' Parameter (with two sigs): </b><br />',
+            NORMAL_STYLE)]
+
+        # two signatures (default behaviour)
+        tables = get_signature_tables(
+            ['Signature 1', 'Signature 2'], horizontal=None)
+        data = self.add_tables(
+            data, '<br /><b>Two Signatures (default, horizontal=None):</b> '
+            '<font face="Times">get_signature_tables(["Signature 1",'
+            ' "Signature 2"])</font></b>',
+            tables)
+
+        # two sigs, both horizontal
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2'], horizontal=True)
+        data = self.add_tables(
+            data,
+            '<br /><b>Two Signatures (horizontal=True):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2"], horizontal=True)</font>',
+            tables)
+
+        # two sigs, both vertical
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2'], horizontal=False)
+        data = self.add_tables(
+            data,
+            '<br /><b>Two Signatures (vertical, horizontal=False):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2"], horizontal=False)</font>',
+            tables)
+
+
+        data += [PageBreak(), Paragraph(
+            '<br /><br /><font size="14"><font face="Times">max_per_row</font>'
+            ' Parameter: </b><br />',
+            NORMAL_STYLE)]
+
+        # four signature (default)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'])
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (default, max_per_row=3):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"])</font>',
+            tables)
+
+        # four signature (max_per_row=1)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=1)
+
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=1):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=1)</font>'
+            '<br /><i>Please note: mode switches from vertical to '
+            'horizontal automatically</i>',
+            tables)
+
+        # four signature (max_per_row=2)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=2)
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=2):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=2)</font>',
+            tables)
+
+        data += [PageBreak()]
+
+        # four signature (max_per_row=4)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=4)
+        data = self.add_tables(
+            data,
+            '<b>Four Sigs (max_per_row=4):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=4)</font>',
+            tables)
+
+        # four signature (max_per_row=5)
+        tables = get_signature_tables(
+            ['Sig 1', 'Sig 2', 'Sig 3', 'Sig 4'], max_per_row=5)
+        data = self.add_tables(
+            data,
+            '<br /><b>Four Sigs (max_per_row=5):</b> '
+            '<font face="Times">get_signature_tables('
+            '["Sig 1", "Sig 2", "Sig 3", "Sig 4"], max_per_row=5)</font>',
+            tables)
+
+
+        # create PDF from data
+        path = draw_sample_pdf(data, title=u"Signatures Sample Sheet")
+        print "Sample PDF written to %s" % path
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_permissions.py	(revision 16045)
@@ -0,0 +1,211 @@
+## $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
+##
+"""
+Permission tests.
+
+Check accessibility of views, pages, viewlets.
+
+"""
+import shutil
+import tempfile
+from zc.async.testing import wait_for_result
+from zope.app.testing.functional import HTTPCaller as http
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.component import createObject, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.app import University
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers,
+    remove_logger)
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.tests.test_authentication import SECRET
+
+
+manager_pages = [
+    # The pages that should only be accessible by manager...
+    '/@@manage', '/@@administration', '/faculties/@@search',
+    '/users/@@index', '/users/@@add', '/users/alice/@@manage',
+    '/datacenter/@@index', '/datacenter/@@upload', '/datacenter/@@import1',
+    '/datacenter/@@import2', '/datacenter/@@import3', '/datacenter/@@import4',
+    '/datacenter/@@logs', '/datacenter/@@show', '/datacenter/@@manage',
+    '/faculties/@@index', '/faculties/@@add', '/faculties/@@manage',
+    '/faculties/fac1/@@index', '/faculties/fac1/@@manage',
+    '/faculties/fac1/@@add',
+    '/faculties/fac1/dept1/@@index',
+    '/faculties/fac1/dept1/@@manage',
+    '/faculties/fac1/dept1/@@addcourse',
+    '/faculties/fac1/dept1/@@addcertificate',
+    '/faculties/fac1/dept1/courses/crs1/@@index',
+    '/faculties/fac1/dept1/courses/crs1/@@manage',
+    '/faculties/fac1/dept1/certificates/cert1/@@index',
+    '/faculties/fac1/dept1/certificates/cert1/@@manage',
+    '/faculties/fac1/dept1/certificates/cert1/@@addcertificatecourse',
+    '/faculties/fac1/dept1/certificates/cert1/crs1_100/@@index',
+    '/faculties/fac1/dept1/certificates/cert1/crs1_100/@@manage',
+    ]
+public_pages = [
+    # Pages accessible also by the public...
+    '/@@index', '/@@login', '/@@logout',
+    ]
+
+class PermissionTest(FunctionalAsyncTestCase, FunctionalTestCase):
+    """Here we try to request all pages and check, whether they are
+    accessible.
+    """
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PermissionTest, self).setUp()
+        # Set up a complete university to have every page available...
+        app = University()
+        self.getRootFolder()['app'] = app
+        setSite(self.getRootFolder()['app'])
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+        app['users'].addUser('alice', 'alice')
+        fac1 = createObject('waeup.Faculty')
+        fac1.code = "fac1"
+        app['faculties'].addFaculty(fac1)
+        dept = createObject('waeup.Department')
+        dept.code = 'dept1'
+        fac1.addDepartment(dept)
+        course = createObject('waeup.Course')
+        course.code = 'crs1'
+        dept.courses.addCourse(course)
+        cert = createObject('waeup.Certificate')
+        cert.code = 'cert1'
+        dept.certificates.addCertificate(cert)
+        cert.addCertCourse(course)
+        self.app = app
+
+        self.browser = Browser()
+        self.browser.handleErrors = False
+        pass
+
+    def tearDown(self):
+        super(PermissionTest, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def isAccessible(self, path):
+        path = 'http://localhost/app%s' % path
+        try:
+            self.browser.open(path)
+            return True
+        except Unauthorized:
+            return False
+        return
+
+    def wait_for_report_job_completed(self, number):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[number][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self, session):
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['student_stats']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="breakdown").value = ['depcode']
+        self.browser.getControl(name="mode").value = ['All']
+        self.browser.getControl(name="session").value = [session]
+        self.browser.getControl("Create").click()
+        return
+
+    def testUnauthenticatedUser(self):
+        for path in manager_pages:
+            if not self.isAccessible(path):
+                continue
+            self.fail('Path %s can be accessed by anonymous.' % path)
+        for path in public_pages:
+            if self.isAccessible(path):
+                continue
+            self.fail('Path %s cannot be accessed by anonymous.' % path)
+        return
+
+    def testReportsPermissions(self):
+        # Create reports officer
+        self.app['users'].addUser('mrofficer', SECRET)
+        self.app['users']['mrofficer'].email = 'mrofficer@foo.ng'
+        self.app['users']['mrofficer'].title = 'Otto Report'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.ReportsOfficer', 'mrofficer')
+        # Create reports manager
+        self.app['users'].addUser('mrmanager', SECRET)
+        self.app['users']['mrmanager'].email = 'mrmanager@foo.ng'
+        self.app['users']['mrmanager'].title = 'Manfred Report'
+        prmglobal.assignRoleToPrincipal('waeup.ReportsManager', 'mrmanager')
+        # The reports officer creates a report which the reports manager
+        # can see.
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'mrofficer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.trigger_report_creation('2004')
+        job_id = self.wait_for_report_job_completed(0)
+        self.browser.open('http://localhost/app/reports')
+        self.assertTrue(
+            'Student Statistics (depcode, 0, All, 2004)'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/logout')
+        # The reports manager creates a report which the reports officer
+        # can't see.
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'mrmanager'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.trigger_report_creation('2005')
+        job_id = self.wait_for_report_job_completed(1)
+        self.browser.open('http://localhost/app/reports')
+        # Manager can see both reports.
+        self.assertTrue(
+            'Student Statistics (depcode, 0, All, 2004)'
+            in self.browser.contents)
+        self.assertTrue(
+            'Student Statistics (depcode, 0, All, 2005)'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/logout')
+        self.browser.open('http://localhost/app/login')
+        self.browser.getControl(name="form.login").value = 'mrofficer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.browser.open('http://localhost/app/reports')
+        # Officer can only see his report.
+        self.assertTrue(
+            'Student Statistics (depcode, 0, All, 2004)'
+            in self.browser.contents)
+        self.assertFalse(
+            'Student Statistics (depcode, 0, All, 2005)'
+            in self.browser.contents)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_reports.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_reports.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/tests/test_reports.py	(revision 16045)
@@ -0,0 +1,65 @@
+"""Tests for browser/reports.py.
+
+Most report related things are in fact tested in `students`.
+
+Here we only test the bits that are not covered otherwise.
+"""
+import shutil
+import tempfile
+from zope.component.hooks import clearSite
+from zope.testbrowser.testing import Browser
+from waeup.kofa.app import University
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, expensive_actions,
+    )
+
+
+class TestReportsContainerCreate(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(TestReportsContainerCreate, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.browser = Browser()
+        self.browser.handleErrors = True
+
+    def tearDown(self):
+        super(TestReportsContainerCreate, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+    def test_block_when_system_load_is_high(self):
+        # under high system load we do not generate new reports
+        assert self.app['reports'] is not None  # a reports container exists
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        with expensive_actions() as utils:
+            utils.expensive_actions_allowed = lambda: False
+            self.browser.open('http://localhost/app/reports/@@create')
+        # we get a warning message about system load
+        self.assertTrue(
+            'high system load' in self.browser.contents)
+        # we are not redirected to some generator, but back to overview
+        self.assertEqual(
+            self.browser.url, 'http://localhost/app/reports')
+
+    def test_no_block_when_system_load_low(self):
+        # under low system load we can pick a generator
+        assert self.app['reports'] is not None  # a reports container exists
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        with expensive_actions() as utils:
+            utils.expensive_actions_allowed = lambda: True
+            self.browser.open('http://localhost/app/reports/@@create')
+        # we do not get a warning message about system load
+        self.assertTrue(
+            'high system load' not in self.browser.contents)
+        # we stay on the page
+        self.assertEqual(
+            self.browser.url, 'http://localhost/app/reports/@@create')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/viewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/viewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/browser/viewlets.py	(revision 16045)
@@ -0,0 +1,1171 @@
+## $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
+##
+import grok
+from urllib import urlencode
+from zope.component import getMultiAdapter, queryAdapter, getUtility
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.location.interfaces import ISite
+from zope.traversing.browser import absoluteURL
+from waeup.kofa.browser.interfaces import (
+    IFacultiesContainer, IFaculty, IDepartment, ICourse, ICertificate,
+    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUsersContainer)
+from waeup.kofa.browser.layout import KofaPage, default_primary_nav_template
+from waeup.kofa.browser.pages import (
+    UniversityPage, FacultiesContainerPage, DatacenterPage, FacultyPage,
+    DepartmentPage, CoursePage, CertificatePage, CertificateCoursePage,
+    UsersContainerPage, UserManageFormPage, DatacenterImportStep1)
+from waeup.kofa.students.browser import EditScoresPage
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import (
+    IKofaUtils, IKofaObject,
+    IDataCenter, IUserAccount,
+    academic_sessions_vocab)
+from waeup.kofa.utils.helpers import get_user_account
+
+
+grok.templatedir('templates')
+grok.context(IKofaObject) # Make IKofaObject the default context
+
+
+class ManageSidebar(grok.ViewletManager):
+    grok.name('left_manage')
+
+
+class BreadCrumbManager(grok.ViewletManager):
+    grok.name('breadcrumbs')
+
+
+class LanguageManager(grok.ViewletManager):
+    grok.name('languages')
+
+
+class ActionBar(grok.ViewletManager):
+    grok.name('actionbar')
+
+
+class AdministrationTasks(grok.ViewletManager):
+    grok.name('admintasks')
+
+
+class WidgetsSlot(grok.ViewletManager):
+    grok.name('widgets')
+
+
+class SourcesLeft(grok.ViewletManager):
+    grok.name('sources_left')
+
+
+class SourcesRight(grok.ViewletManager):
+    grok.name('sources_right')
+
+
+#
+# Baseclasses that give some defaults for really used viewlets.
+#
+class ActionButton(grok.Viewlet):
+    """A base for action buttons.
+
+    An action button provides an icon, some text, links to a
+    target and optionally an onclick event handler.
+    If you want to set a different text, icon or target name
+    for some active button below, just override the approriate
+    attribute in the concerned viewlet.
+
+    Action buttons provide by default dynamic attributes
+
+     * ``alt``
+          An alternative text for the icon. By default the same as
+          the text.
+
+     * ``icon_url``
+          The URL of the icon.
+
+     * ``target_url``
+          The URL of the link target.
+
+     * ``onclick``
+          An onclick Javascript event handler.
+
+    """
+    grok.baseclass()
+    grok.context(IKofaObject)
+    grok.viewletmanager(ActionBar)
+    icon = 'actionicon_modify.png' # File must exist in static/
+    target = '@@manage' # link to this viewname.
+    text = _('Edit') # Text to display on the button
+
+    # We set the template file explicitly (instead of using
+    # ``grok.template('actionbutton')``) to stick with this template
+    # also in derived classes in other packages. If we didn't, those
+    # derived ActionButton viewlets had to provide an own template,
+    # which would not be updated automatically, when the local
+    # template ``templates/actionbutton.pt`` changes.
+    #
+    # Inheriting viewlets that wish to use their own template anyway
+    # can do so by setting their local ``grok.template(<mytemplate>)``
+    # and setting ``template`` to ``None`` for the class::
+    #
+    # class DerivedActionButton(ActionButton):
+    #   ...
+    #   grok.template('overriding_template')
+    #   template = None
+    #   ...
+    #
+    template = grok.PageTemplateFile('templates/actionbutton.pt')
+
+    @property
+    def alt(self):
+        """Alternative text for icon.
+        """
+        return self.text
+
+    @property
+    def icon_url(self):
+        """Get the icon URL.
+        """
+        return '/static/img/%s' % self.icon
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        if self.target:
+            return self.view.url(self.view.context, self.target)
+        return
+
+    @property
+    def onclick(self):
+        """Onclick event...
+        """
+        return
+
+class PlainActionButton(ActionButton):
+    """A base for action buttons without image
+    """
+    grok.baseclass()
+    template = grok.PageTemplateFile('templates/plainactionbutton.pt')
+
+    
+class ManageActionButton(ActionButton):
+    """A base for 'edit' buttons
+    """
+    grok.baseclass()
+    grok.order(2)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_modify.png'
+    target = '@@manage'
+    text = _('Edit')
+
+class AddActionButton(ActionButton):
+    """A base for 'add' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_add.png'
+    target = 'add'
+    text = _('Add')
+    
+class RemoveActionButton(ActionButton):
+    """A base for 'remove' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_delete.png'
+    target = 'remove'
+    text = _('Remove')
+
+class SearchActionButton(ActionButton):
+    """A base for 'search' buttons.
+    """
+    grok.baseclass()
+    grok.order(5)
+    grok.require('waeup.manageAcademics')
+    icon = 'actionicon_search.png'
+    target = 'search'
+    text = _('Search')
+
+
+#
+# General viewlets (for more than one page/context)
+#
+
+class BreadCrumbs(grok.Viewlet):
+    grok.context(IKofaObject)
+    grok.viewletmanager(BreadCrumbManager)
+    grok.order(1)
+
+    def getEntries(self):
+        result = []
+        site = grok.getSite()
+        context = self.context
+        breadcrumbs = IBreadcrumbContainer(self.view)
+        for breadcrumb in breadcrumbs:
+            if breadcrumb.target is None:
+                yield dict(
+                    title = breadcrumb.title,
+                    url = self.view.url(breadcrumb.context)
+                    )
+            elif breadcrumb.target:
+                yield dict(
+                    title = breadcrumb.title,
+                    url = self.view.url(breadcrumb.context, breadcrumb.target)
+                    )
+
+class LanguagesLink(grok.Viewlet):
+    """ The language selector itself.
+    """
+    grok.viewletmanager(LanguageManager)
+    grok.context(IKofaObject)
+    grok.require('waeup.Public')
+    title = u'Languages'
+
+    def render(self):
+        preferred_languages = getUtility(IKofaUtils).PREFERRED_LANGUAGES_DICT
+        html = u''
+        for key, value in sorted(
+            preferred_languages.items(), key=lambda lang: lang[1][0]):
+            args = {'lang':key, 'view_name':self.view.__name__}
+            url = self.view.url(
+                self.context) + '/@@change_language?%s' % urlencode(args)
+            html += u'| <a href="%s" title="%s">%s</a> ' % (url, value[1], key)
+        return html
+
+
+class WidgetsTableRows(grok.Viewlet):
+    """The only viewlet for the WidgetsSlot viewlet manager.
+    """
+    template = grok.PageTemplateFile('templates/widgets.pt')
+    grok.viewletmanager(WidgetsSlot)
+
+
+class Login(grok.Viewlet):
+    """This viewlet allows to login in the sidebar.
+    """
+    grok.viewletmanager(ManageSidebar)
+    grok.context(IKofaObject)
+    grok.view(Interface)
+    grok.order(2)
+    grok.require('waeup.Anonymous')
+    text = _('Login')
+    link = 'login'
+
+    def render(self):
+        if self.request.principal.id != 'zope.anybody':
+            return ''
+        url = self.view.url(grok.getSite(), self.link)
+        return u'<li"><a href="%s">%s</a></li>' % (
+                url, self.text)
+
+
+class ManageLink(grok.Viewlet):
+    """A link displayed in the upper left box.
+
+    This viewlet renders a link to the application object's settings
+    form (the 'manage' view).
+
+    In derived classes you can create different links by setting a
+    different link and text attribute. The `link` parameter is
+    understood relative to the respective application object, so that
+    ``@@manage`` will create a link to
+    ``localhost:8080/app/@@manage``.
+
+    Links defined by descendants from this viewlet are displayed on
+    every page the user is allowed to go to, if the user has also the
+    permissions set by `grok.require()`. By default only users with
+    ``waeup.managePortal`` permission will see links defined by
+    this or derivated classes.
+    """
+    grok.baseclass()
+    grok.viewletmanager(ManageSidebar)
+    grok.context(IKofaObject)
+    grok.view(Interface)
+    grok.order(1)
+    # This link is only displayed, if the user is
+    # allowed to use it!
+    grok.require('waeup.managePortal')
+
+    link = 'relative_link'
+    text = _(u'Any link text')
+    
+    def render(self):
+        url = self.view.url(grok.getSite(), self.link)
+        text = translate(self.text, context=self.request)
+        return u'<li><a href="%s">%s</a></li>' % (url, text)
+
+class ManagePortalConfiguration(ManageLink):
+    """A link to portal configuration.
+    """
+    grok.order(1)
+    grok.require('waeup.managePortalConfiguration')
+
+    link = 'configuration'
+    text = _(u'Portal Configuration')
+
+class ManageUsersLink(ManageLink):
+    """A link to users management, placed in upper left box.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUsers')
+
+    link = u'users'
+    text = _(u'Officers')
+
+class ManageDataCenter(ManageLink):
+    """A link to datacenter, placed in upper left box.
+    """
+    grok.order(3)
+    grok.require('waeup.manageDataCenter')
+
+    link = u'datacenter'
+    text = _(u'Data Center')
+
+class ManageReports(ManageLink):
+    """A link to reports, placed in upper left box.
+    """
+    grok.order(4)
+    grok.require('waeup.handleReports')
+
+    link = u'reports'
+    text = _(u'Reports')
+
+class LecturerCourses(ManageLink):
+    """A link to courses of lecturers.
+    """
+    grok.order(6)
+    grok.require('waeup.Authenticated')
+
+    link = u'my_courses'
+    text = _(u'My Courses')
+
+    def render(self):
+        user_account = get_user_account(self.request)
+        if user_account:
+            local_roles = user_account.getLocalRoles()
+            if local_roles.has_key('waeup.local.Lecturer'):
+                url = self.view.url(grok.getSite(), self.link)
+                text = translate(self.text, context=self.request)
+                return u'<li><a href="%s">%s</a></li>' % (url, text)
+        return ''
+
+class TranscriptOfficerStudents(ManageLink):
+    """A link to transcript requests.
+    """
+    grok.order(6)
+    grok.require('waeup.Authenticated')
+
+    link = u'my_transcript_requests'
+    text = _(u'My ToDoList')
+
+    def render(self):
+        user_account = get_user_account(self.request)
+        if user_account:
+            local_roles = user_account.getLocalRoles()
+            if local_roles.has_key('waeup.local.TranscriptOfficer'):
+                url = self.view.url(grok.getSite(), self.link)
+                text = translate(self.text, context=self.request)
+                return u'<li><a href="%s">%s</a></li>' % (url, text)
+        return ''
+
+class TranscriptSigneeStudents(ManageLink):
+    """A link to transcript signing requests.
+    """
+    grok.order(6)
+    grok.require('waeup.Authenticated')
+
+    link = u'my_transcript_signing_requests'
+    text = _(u'My ToDoList')
+
+    def render(self):
+        user_account = get_user_account(self.request)
+        if user_account:
+            local_roles = user_account.getLocalRoles()
+            if local_roles.has_key('waeup.local.TranscriptSignee'):
+                url = self.view.url(grok.getSite(), self.link)
+                text = translate(self.text, context=self.request)
+                return u'<li><a href="%s">%s</a></li>' % (url, text)
+        return ''
+
+class MyPreferences(ManageLink):
+    """A link to personal preferences, placed in upper left box.
+    """
+    grok.order(7)
+    grok.require('waeup.Public')
+    text = _(u'My Preferences')
+
+    def render(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            url = self.view.url(account_object)
+            text = translate(self.text, context=self.request)
+            return u'<li><a href="%s">%s</a></li>' % (url, text)
+        return ''
+
+class MyRoles(ManageLink):
+    """A link to display site and local roles.
+    """
+    grok.order(8)
+    grok.require('waeup.Public')
+    text = _(u'My Roles')
+
+    def render(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            url = self.view.url(account_object) + '/my_roles'
+            text = translate(self.text, context=self.request)
+            return u'<li><a href="%s">%s</a></li>' % (url, text)
+        return ''
+
+class ContactActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IUserAccount)
+    grok.view(UserManageFormPage)
+    grok.require('waeup.manageUsers')
+    icon = 'actionicon_mail.png'
+    text = _('Send email')
+    target = 'contactuser'
+
+class ManageDataCenterActionButton(ManageActionButton):
+    """ 'Edit settings' button for datacenter.
+    """
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.managePortal')
+    text = _('Edit settings')
+    grok.order(1)
+
+class ManageFacultiesContainerActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFacultiesContainer)
+    grok.view(FacultiesContainerPage)
+    text = _('Manage academic section')
+
+class ExportFacultiesStudentsActionButton(ManageActionButton):
+    """ 'Export student data' button for faculties.
+    """
+    grok.context(IFacultiesContainer)
+    grok.view(FacultiesContainerPage)
+    grok.name('exportfacultystudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_down.png'
+    text = _('Export student data')
+    target = 'exports'
+    grok.order(3)
+
+class SearchFacultiesContainerActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFacultiesContainer)
+    grok.view(FacultiesContainerPage)
+    text = _('Search academic section')
+    icon = 'actionicon_search.png'
+    target = '@@search'
+    
+class ManageFacultyActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFaculty)
+    grok.view(FacultyPage)
+    text = _('Manage faculty')
+
+class StudentSearchActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IFaculty)
+    grok.view(FacultyPage)
+    grok.name('findstudents')
+    grok.require('waeup.showStudents')
+    text = _('Find students')
+    icon = 'actionicon_search.png'
+    target = 'find_students'
+
+class ExportFacultyStudentsActionButton(ManageActionButton):
+    """ 'Export student data' button for faculties.
+    """
+    grok.order(3)
+    grok.context(IFaculty)
+    grok.view(FacultyPage)
+    grok.name('exportfacultystudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_down.png'
+    text = _('Export student data')
+    target = 'exports'
+
+class ManageDepartmentActionButton(ManageActionButton):
+    """ 'Manage settings' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    text = _('Manage department')
+    grok.order(1)
+
+class ShowDepartmentStudentsActionButton(ManageActionButton):
+    """ 'Show students' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.name('showdepartmentstudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_student.png'
+    text = _('Show students')
+    target = 'students'
+    grok.order(2)
+
+class ExportDepartmentStudentsActionButton(ManageActionButton):
+    """ 'Export student data' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.name('exportdepartmentstudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_down.png'
+    text = _('Export student data')
+    target = 'exports'
+    grok.order(3)
+
+class ClearDepartmentStudentsActionButton(ManageActionButton):
+    """ 'Clear all students' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.name('cleardepartmentstudents')
+    grok.require('waeup.clearAllStudents')
+    icon = 'actionicon_accept.png'
+    text = _('Clear all students')
+    target = 'clearallstudents'
+    grok.order(4)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'All students, who requested clearance in this department, will be cleared. Are you sure?'")
+
+class ManageCourseActionButton(ManageActionButton):
+    """ 'Edit settings' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    text = _('Edit course')
+    grok.order(1)
+    
+class ShowCourseStudentsActionButton(ManageActionButton):
+    """ 'Show students' button in course.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('showcoursestudents')
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_student.png'
+    text = _('Show students')
+    target = 'students'
+    grok.order(2)
+
+class ExportCourseStudentsActionButton(ManageActionButton):
+    """ 'Export student data' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('exportcoursestudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_down.png'
+    text = _('Export student data')
+    target = 'exports'
+    grok.order(3)
+
+class UpdateScoresActionButton(ManageActionButton):
+    """ 'Update scores' button in course.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('updatescores')
+    grok.require('waeup.editScores')
+    icon = 'actionicon_scores.png'
+    target = 'edit_scores'
+    grok.order(4)
+
+    @property
+    def target_url(self):
+        ca = grok.getSite()[
+            'configuration'].current_academic_session
+        if not ca:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def text(self):
+        ca = grok.getSite()[
+            'configuration'].current_academic_session
+        st = academic_sessions_vocab.getTerm(ca).title
+        return _('Update session ${a} scores', mapping = {'a':st})
+
+class DownloadTicketOverviewActionButton(ManageActionButton):
+    """ 'Download ticket overview' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('coursetickets')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_pdf.png'
+    text = _('Download course tickets overview')
+    target = 'coursetickets.pdf'
+    grok.order(5)
+
+class DownloadAttendanceSheetActionButton(ManageActionButton):
+    """ 'Download ticket overview' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('attendance')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_pdf.png'
+    text = _('Download attendance sheet')
+    target = 'attendance.pdf'
+    grok.order(6)
+
+class UpdateCourseTicketsActionButton(ManageActionButton):
+    """ 'Update credits in course tickets' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    grok.name('updatecoursetickets')
+    grok.require('waeup.editScores')
+    icon = 'actionicon_reload.png'
+    target = 'update_coursetickets'
+    grok.order(7)
+
+    @property
+    def target_url(self):
+        ca = grok.getSite()[
+            'configuration'].current_academic_session
+        if not ca:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def text(self):
+        ca = grok.getSite()[
+            'configuration'].current_academic_session
+        st = academic_sessions_vocab.getTerm(ca).title
+        return _('Update session ${a} credits', mapping = {'a':st})
+
+    @property
+    def onclick(self):
+        current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if not current_academic_session:
+            return
+        message = _("All current session (%s/%s) course tickets of students, "
+                    "who paid school fee but not yet registered their courses, "
+                    "will be updated.\\n\\n"
+                    "You really want to update?") % (
+                        current_academic_session, current_academic_session+1)
+        return "return window.confirm('%s');" % message
+
+class DownloadCSVFileActionButton(ManageActionButton):
+    """ 'Download csv file' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(EditScoresPage)
+    grok.name('downloadcsv')
+    grok.require('waeup.editScores')
+    icon = 'actionicon_down.png'
+    text = _('Download csv file (editable scores only)')
+    target = 'download_scores'
+    grok.order(1)
+
+class ManageCertificateActionButton(ManageActionButton):
+    """ 'Manage settings' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    text = _('Manage certificate')
+    grok.order(1)
+
+class ShowCertificateStudentsActionButton(ManageActionButton):
+    """ 'Show students' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    grok.name('showcertificatestudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_student.png'
+    text = _('Show students')
+    target = 'students'
+    grok.order(2)
+
+class ExportCertificateStudentsActionButton(ManageActionButton):
+    """ 'Export student data' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    grok.name('exportcertificatestudents')
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_down.png'
+    text = _('Export student data')
+    target = 'exports'
+    grok.order(3)
+
+class ManageCertificateCourseActionButton(ManageActionButton):
+    """ 'Manage settings' button for certificate courses.
+    """
+    grok.context(ICertificateCourse)
+    grok.view(CertificateCoursePage)
+    text = _('Edit certificate course')
+
+class AddUserActionButton(AddActionButton):
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.view(UsersContainerPage)
+    text = _('Add officer')
+
+class BrowseDatacenterLogs(ActionButton):
+    grok.context(IDataCenter)
+    grok.require('waeup.manageDataCenter')
+    grok.view(DatacenterPage)
+    grok.order(2)
+    icon = 'actionicon_info.png'
+    target = '@@logs'
+    text = _('Show logs')
+
+class UploadCSVButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageDataCenter')
+    grok.order(3)
+    icon = 'actionicon_up.png'
+    target = '@@upload'
+    text = _('Upload data')
+
+class BatchOpButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageDataCenter')
+    grok.order(4)
+    icon = 'actionicon_gear.png'
+    target = '@@import1'
+    text = _('Process data')
+
+class ExportCSVButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.exportData')
+    grok.order(5)
+    icon = 'actionicon_down.png'
+    target = '@@export'
+    text = _('Export data')
+
+class BrowseFinishedFiles(ActionButton):
+    grok.context(IDataCenter)
+    grok.require('waeup.manageDataCenter')
+    grok.view(DatacenterPage)
+    grok.order(6)
+    icon = 'actionicon_finished.png'
+    target = '@@processed'
+    text = _('View processed files')
+
+class SwitchMaintMode(ActionButton):
+    grok.context(IDataCenter)
+    grok.require('waeup.importData')
+    #grok.view(DatacenterImportStep1)
+    grok.order(7)
+    icon = 'actionicon_stop.png'
+    target = 'switchmaintmode'
+    text = _('Switch maintenance mode')
+
+    @property
+    def onclick(self):
+        if grok.getSite()['configuration'].maintmode_enabled_by:
+            return
+        return "return window.confirm(%s);" % _(
+            "'In maintenance mode no other user can login, and "
+            "already logged-in users will be automatically logged out. "
+            "You will be the only logged-in user and you can safely start "
+            "any import. Please wait a few seconds before starting the import "
+            "so that all running processes are finished. "
+            "If the import is done, maintenance mode will "
+            "be automatically disabled. \\n\\n"
+            "You really want to enable maintenance mode?'")
+
+#
+# Primary navigation tabs (in upper left navigation bar)...
+#
+class PrimaryNavManager(grok.ViewletManager):
+    """Viewlet manager for the primary navigation tab.
+    """
+    grok.name('primary_nav')
+
+class PrimaryNavTab(grok.Viewlet):
+    """Base for primary nav tabs.
+    """
+    grok.baseclass()
+    grok.viewletmanager(PrimaryNavManager)
+    grok.order(1)
+    grok.require('waeup.Public')
+    template = default_primary_nav_template
+
+    pnav = 0 # This is a kind of id of a tab. If some page provides
+             # also a 'pnav' attribute with the same value (here: 0),
+             # then the tab will be rendered as 'active' when the page
+             # gets rendered.
+             #
+             # This way you can assign certain pages to certain
+             # primary nav tabs. Each primary tab should therefore set
+             # the 'pnav' attribute to a different value (or several
+             # tabs might be rendered as active simultanously when the
+             # page gets rendered.
+    tab_title = u'Some Text'
+    
+    @property
+    def link_target(self):
+        return self.view.application_url()
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active'
+        return ''
+
+#class HomeTab(PrimaryNavTab):
+#    """Home-tab in primary navigation.
+#    """
+#    grok.order(1)
+#    grok.require('waeup.Public')
+
+#    pnav = 0
+#    tab_title = u'Home'
+
+class FacultiesTab(PrimaryNavTab):
+    """Faculties-tab in primary navigation.
+    """
+    grok.order(2)
+    grok.require('waeup.viewAcademics')
+
+    pnav = 1
+    tab_title = _(u'Academics')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('faculties')
+
+    
+class EnquiriesTab(PrimaryNavTab):
+    """Contact tab in primary navigation.
+
+    Display tab only for anonymous. Authenticated users can call a
+    contact form from the user navigation bar.
+    """
+    grok.order(6)
+    grok.require('waeup.Anonymous')
+    tab_title = _(u'Enquiries')
+    pnav = 2
+
+    # Also zope.manager has role Anonymous.
+    # To avoid displaying this tab, we have to check the principal id too.
+    @property
+    def link_target(self):
+        if self.request.principal.id == 'zope.anybody':
+            return self.view.application_url('enquiries')
+        return
+
+#
+# Administration tasks
+#
+class AdminTask(grok.Viewlet):
+    """The base for task entries on administration page.
+    """
+    grok.baseclass()
+    grok.order(1)
+    grok.viewletmanager(AdministrationTasks)
+    grok.require('waeup.managePortal')
+    grok.template('admintask')
+
+    link_title = 'Manage users' # How the link to the target will be titled.
+    target_viewname = 'users'   # The name of the target view.
+    
+    @property
+    def link_target(self):
+        return self.view.url(self.context[self.target_viewname])
+
+class AdminTaskPortalConfiguration(AdminTask):
+    """Entry on administration page that link to portal settings.
+    """
+    grok.order(1)
+    grok.require('waeup.managePortalConfiguration')
+
+    link_title = _('Portal Configuration')
+    def link_target(self):
+        return self.view.url(self.view.context, 'configuration')
+
+class AdminTaskUsers(AdminTask):
+    """Entry on administration page that link to user folder.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUsers')
+
+    link_title = _('Officers')
+    target_viewname = 'users'
+    
+class AdminTaskDatacenter(AdminTask):
+    """Entry on administration page that link to datacenter.
+    """
+    grok.order(3)
+    grok.require('waeup.manageDataCenter')
+
+    link_title = _('Data Center')
+    target_viewname = 'datacenter'
+
+class AdminTaskReports(AdminTask):
+    """Entry on administration page that link to datacenter.
+    """
+    grok.order(3)
+    grok.require('waeup.manageReports')
+
+    link_title = _('Reports')
+    target_viewname = 'reports'
+
+# The SubobjectLister and its viewlets below are not used in Kofa.
+
+class SubobjectLister(grok.ViewletManager):
+    """Very special viewlet manager that displays lists of subobjects.
+    """
+    grok.name('subobjectlist')
+    grok.template('subobjectlist')
+
+    def update(self):
+        # The default implementation of update() sets self.viewlets to
+        # a list of viewlets for the current context
+        # (self.context). We make use of that fact by retrieving all
+        # viewlets for all items in our context container by simply
+        # setting these items as context while we call the default
+        # update() method. So we get a list of lists of viewlets for
+        # each item in a 'row' (where a single item is a row).
+        rows = []
+        orig_context = self.context
+        for name, value in self.context.items():
+            # Retrieve all viewlets for the current item (not the context)
+            self.context = value
+            super(SubobjectLister, self).update() # sets self.viewlets
+            rows.append(self.viewlets)
+            self.context = orig_context
+        self.rows = rows
+        # Finally, set the viewlets we would retrieve normally...
+        super(SubobjectLister, self).update()
+        return
+
+
+class FacultiesContainerListHead(grok.Viewlet):
+    """The header line of faculty container subobject lists.
+    """
+    grok.order(1)
+    grok.viewletmanager(SubobjectLister)
+    grok.context(IFacultiesContainer)
+    grok.require('waeup.viewAcademics')
+
+    def render(self):
+        return u'<th>Code</th><th>Title</th><th></th>'
+
+class FacultyListName(grok.Viewlet):
+    """Display a the title of a faculty as link in a list.
+    """
+    grok.order(1)
+    grok.viewletmanager(SubobjectLister)
+    grok.context(IFaculty)
+    grok.require('waeup.viewAcademics')
+
+    def render(self):
+        return u'<a href="%s">%s</a>' % (
+            self.view.url(self.context), self.context.__name__)
+
+class FacultyListTitle(grok.Viewlet):
+    """Display the title of a faculty in a list.
+    """
+    grok.order(2)
+    grok.viewletmanager(SubobjectLister)
+    grok.context(IFaculty)
+    grok.require('waeup.viewAcademics')
+
+    def render(self):
+        return self.context.title
+
+class FacultyRemoveButton(grok.Viewlet):
+    """Render a remove button for faculty lists.
+    """
+    grok.order(3)
+    grok.viewletmanager(SubobjectLister)
+    grok.context(IFaculty)
+    grok.require('waeup.manageAcademics')
+
+    def render(self):
+        return u'<div class="text-right"><form method="POST">' + \
+            '<input class="text-right" type="submit" value="remove" />' + \
+            '</form></div>'
+
+class InstTypesSource(grok.Viewlet):
+    """Available institution types for the sources overview page.
+    """
+    grok.order(1)
+    grok.viewletmanager(SourcesLeft)
+    grok.require('waeup.Public')
+    grok.template('source')
+    column = 'left'
+    name = 'InstTypes'
+    source_name = 'inst_types'
+    title = _('Institution Types')
+
+    def heading(self):
+        return 'heading%s' % self.name
+
+    def accordion(self):
+        return '#accordion-%s' % self.column
+
+    def collapse(self):
+        return 'collapse%s' % self.name
+
+    def hash_collapse(self):
+        return '#collapse%s' % self.name
+
+    def source(self):
+        return getattr(self.view, self.source_name)
+
+class SessionSource(InstTypesSource):
+    """Available sessions for the sources overview page.
+    """
+    grok.order(2)
+    name = 'Sessions'
+    source_name = 'sessions'
+    title = _('Academic Sessions')
+
+class CourseLevelSource(InstTypesSource):
+    """Available course levels for the sources overview page.
+    """
+    grok.order(3)
+    name = 'CourseLevels'
+    source_name = 'course_levels'
+    title = _('Course Levels')
+
+class StudyLevelSource(InstTypesSource):
+    """Available study levels for the sources overview page.
+    """
+    grok.order(4)
+    name = 'StudyLevels'
+    source_name = 'study_levels'
+    title = _('Study Levels')
+
+class StudyModeSource(InstTypesSource):
+    """Available study modes for the sources overview page.
+    """
+    grok.order(5)
+    name = 'StudyModes'
+    source_name = 'study_modes'
+    title = _('Study Modes')
+
+class DegreeSource(InstTypesSource):
+    """Available study modes for the sources overview page.
+    """
+    grok.order(6)
+    name = 'Degrees'
+    source_name = 'degrees'
+    title = _('Degrees')
+
+class PaymentCatsSource(InstTypesSource):
+    """Available payment categories for the sources overview page.
+    """
+    grok.order(7)
+    name = 'PaymentCats'
+    source_name = 'payment_cats'
+    title = _('Payment Categories')
+
+class AppTypesSource(InstTypesSource):
+    """Available application types for the sources overview page.
+    """
+    grok.order(8)
+    name = 'AppTypes'
+    source_name = 'app_types'
+    title = _('Application Types (Prefixes)')
+
+class AppCatsSource(InstTypesSource):
+    """Available application categories for the sources overview page.
+    """
+    grok.order(9)
+    name = 'AppCats'
+    source_name = 'app_cats'
+    title = _('Application Categories')
+
+class StudentStatesSource(InstTypesSource):
+    """Available student sates for the sources overview page.
+    """
+    grok.order(1)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'StudentStates'
+    source_name = 'student_states'
+    title = _('Student Registration States')
+
+class VerdictsSource(InstTypesSource):
+    """Available verdicts for the sources overview page.
+    """
+    grok.order(2)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'Verdicts'
+    source_name = 'verdicts'
+    title = _('Verdicts')
+
+class ExamSubjectsSource(InstTypesSource):
+    """Available exam subjects for the sources overview page.
+    """
+    grok.order(4)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'ExamSubjects'
+    source_name = 'exam_subjects'
+    title = _('Exam Subjects')
+
+class ExamGradesSource(InstTypesSource):
+    """Available exam grades for the sources overview page.
+    """
+    grok.order(5)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'ExamGrades'
+    source_name = 'exam_grades'
+    title = _('Exam Grades')
+
+class SexSource(InstTypesSource):
+    """Available sex values for the sources overview page.
+    """
+    grok.order(6)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'Sex'
+    source_name = 'sex'
+    title = _('Sex')
+
+class DisablePaymentGroupSource(InstTypesSource):
+    """Available payment groups for the sources overview page.
+    """
+    grok.order(7)
+    grok.viewletmanager(SourcesRight)
+    column = 'right'
+    name = 'DisablePaymentGroups'
+    source_name = 'disable_payment_groups'
+    title = _('Disable Payment Groups')
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/catalog.py	(revision 16045)
@@ -0,0 +1,168 @@
+## $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
+##
+"""Components to help cataloging and searching objects.
+"""
+import grok
+from hurry.query.interfaces import IQuery
+from hurry.query.query import Query
+from zope.catalog.catalog import ResultSet
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, queryUtility
+from zope.interface import implementer
+from zope.intid.interfaces import IIntIds
+from waeup.kofa.interfaces import (
+    IQueryResultItem, IFilteredQuery, IFilteredCatalogQuery)
+
+# not yet used
+@implementer(IQuery)
+class KofaQuery(Query):
+    """A `hurry.query.query.Query` compatible query that also supports
+    retrival of plain ``Bree`` result sets as used inside a catalog.
+
+    Like `hurry.query.query.Query` objects, `KofaQuery` is some kind
+    of a meta query or 'compound query' that can give the cataloged
+    objects (or their int ids) matching one or more 'subqueries'.
+
+    This way you can search for objects (or their int ids) that match
+    several criteria at the same time. See ``examples`` section below.
+
+    A singleton instance of this class is also available as global
+    utility.
+
+    A hurry.query-like query that supports also ``apply``.
+    """
+    def apply(self, query):
+        """Get the list of int ids (a `BTree` result set) for objects
+        determined by ``query``.
+
+        The list of int ids is less expensive to compute than the
+        complete search results and sufficient, for instance, when you
+        only need the number of objects that match a query and not the
+        objects themselves.
+        """
+        return query.apply()
+
+    def searchResults(self, query):
+        """Get a set of ZODB objects conforming to a query.
+        """
+        results = self.apply(query)
+        if results is not None:
+            uidutil = getUtility(IIntIds)
+            results = ResultSet(results, uidutil)
+        return results
+
+grok.global_utility(KofaQuery)
+
+# not yet used
+@implementer(IQueryResultItem)
+class QueryResultItem(object):
+    url = None
+    title = None
+    description = None
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.title = context.title
+        self.description = ''
+
+@implementer(IFilteredQuery)
+class FilteredQueryBase(object):
+    """A filter to find objects that match certain parameters.
+
+    Parameters are passed to constructor as keyword arguments. The
+    real data retrieval then happens when `query()` is called.
+
+    The `defaults` attribute, a dict, can set certain default values
+    for parameters that are used if the constructor is called without
+    any parameters.
+    """
+    defaults = dict()
+
+    def __init__(self, **kw):
+        self._kw = dict(self.defaults)
+        self._kw.update(kw)
+        return
+
+    def query(self, context=None):
+        err_msg = 'class %s does not implement the query() method.' % (
+            self.__class__.__name__, )
+        raise NotImplementedError(err_msg)
+
+@implementer(IFilteredCatalogQuery)
+class FilteredCatalogQueryBase(FilteredQueryBase):
+    """Base for filtered queries based on catalog lookups.
+
+    This type of query asks a catalog to find objects.
+
+    You would normally use this type of query like this:
+
+      >>> query = FilteredCatalogQueryBase(name='bob')
+      >>> objects = query.query()
+
+    The name of the catalog to use can be set via `cat_name`
+    attribute.
+
+    Looked up are all objects that match keywords passed to
+    constructor where the keyword names must match a certain index of
+    the chosen catalog. So, if some catalog has indexes `name` and
+    `age`, then keywords `name='bob', age='12'` would search for all
+    objects with name ``bob`` and age ``12``.
+
+    This query supports single values (exact matches) and ranges of
+    values passed in via ``(min_value, max_value)`` tuples. So,
+    constructor keyword args `name=('a', 'd')` would find objects with
+    name ``alice``, ``bob``, ``d``, but not ``donald``, ``john``, or
+    ``zak``.
+    """
+    cat_name = None
+
+    def query_catalog(self, catalog):
+        """Search ``catalog``.
+
+        Use `catalog`, some ``Catalog`` instance, to search objects
+        denoted by constructor keywords.
+        """
+        query = dict()
+        for idx_name, value in self._kw.items():
+            if idx_name == 'catalog':
+                continue
+            if value is not None:
+                if 'session' in idx_name or 'level' in idx_name:
+                    value = int(value)
+                if idx_name in ('level', 'current_level'):
+                    value = int(value)
+                    if value not in (10, 999):
+                        value = (value, value + 90)
+            if not isinstance(value, tuple):
+                value = (value, value)
+            query[idx_name] = value
+        result = catalog.searchResults(**query)
+        return result
+
+    def query(self):
+        """Perform a query with parameters passed to constructor.
+
+        Returns some iterable, normally a list or a catalog result
+        set.
+        """
+        catalog = queryUtility(
+            ICatalog, name=self.cat_name, default=None)
+        if catalog is None:
+            return []
+        return self.query_catalog(catalog)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configuration.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configuration.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configuration.py	(revision 16045)
@@ -0,0 +1,75 @@
+## $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
+##
+"""
+Components for portal configuration.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import (
+    ISessionConfiguration, IConfigurationContainer, ISessionConfigurationAdd,
+    academic_sessions_vocab)
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class ConfigurationContainer(grok.Container):
+    """
+    The node containing the session configuration models
+    """
+
+    grok.implements(IConfigurationContainer)
+
+    def addSessionConfiguration(self, sessionconfiguration):
+        """Add a session configuration object.
+        """
+        if not ISessionConfiguration.providedBy(sessionconfiguration):
+            raise TypeError(
+                'ConfigurationContainers contain only '
+                'ISessionConfiguration instances')
+        code = unicode(sessionconfiguration.academic_session)
+        self[code] = sessionconfiguration
+        return
+
+ConfigurationContainer = attrs_to_fields(ConfigurationContainer)
+
+class SessionConfiguration(grok.Model):
+    """
+    Session configuration model
+    """
+
+    grok.implements(ISessionConfiguration, ISessionConfigurationAdd)
+
+    def getSessionString(self):
+        """Return the session string from the vocabulary.
+        """
+        return academic_sessions_vocab.getTerm(self.academic_session).title
+
+SessionConfiguration = attrs_to_fields(SessionConfiguration)
+
+class SessionConfigurationFactory(grok.GlobalUtility):
+    """A factory for session configuration objects.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.SessionConfiguration')
+    title = u"Create a new session configuration object.",
+    description = u"This factory instantiates new session configurations."
+
+    def __call__(self, *args, **kw):
+        return SessionConfiguration(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(SessionConfiguration)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configure.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configure.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/configure.zcml	(revision 16045)
@@ -0,0 +1,23 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:grok="http://namespaces.zope.org/grok"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include package="waeup.kofa" file="locales.zcml" />
+  <include package="grok" />
+  <include package="waeup.kofa" file="meta.zcml" />
+  <includeDependencies package="." />
+
+  <!-- Comment the following line if you don't want dolmen.beaker,
+       for instance on Windows if you were not able to compile it
+
+       We have to configure dolmen.beaker manually, because it is
+       not part of installation requirements and will therefore not
+       be found by the includeDependencies directive. -->
+  <include package="dolmen.beaker" file="configure.zcml" />
+  <grok:grok package="." />
+
+  <!-- include package="." file="async.zcml" / -->
+  <includeOverrides package="waeup.kofa.widgets" file="overrides.zcml" />
+  <includeOverrides package="waeup.kofa.utils" file="overrides.zcml" />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/datacenter.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/datacenter.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/datacenter.py	(revision 16045)
@@ -0,0 +1,439 @@
+## $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
+##
+"""Kofa data center.
+
+The waeup data center cares for management of upload data and provides
+tools for importing/exporting CSV data.
+"""
+import codecs
+import fnmatch
+import grok
+import os
+import re
+import shutil
+from datetime import datetime
+from zope.component import getUtility
+from zope.component.interfaces import ObjectEvent
+from waeup.kofa.interfaces import (IDataCenter, IDataCenterFile,
+                                   IDataCenterStorageMovedEvent,
+                                   IDataCenterConfig,
+                                   IKofaUtils)
+from waeup.kofa.utils.batching import ExportJobContainer
+from waeup.kofa.utils.helpers import copy_filesystem_tree, merge_csv_files
+from waeup.kofa.utils.logger import Logger
+
+
+#: Regular expression describing a logfile name with backup extension
+RE_LOGFILE_BACKUP_NAME = re.compile('^.+\.\d+$')
+
+class DataCenter(grok.Container, Logger, ExportJobContainer):
+    """A data center contains CSV files.
+    """
+    grok.implements(IDataCenter)
+
+    logger_name = 'waeup.kofa.${sitename}.datacenter'
+    logger_filename = 'datacenter.log'
+
+    @property
+    def max_files(self):
+        return getUtility(IKofaUtils).MAX_FILES
+
+    def __init__(self, *args, **kw):
+        super(DataCenter, self).__init__(*args, **kw)
+        self.storage = getUtility(IDataCenterConfig)['path']
+        self._createSubDirs()
+
+    def _createSubDirs(self):
+        """Create standard subdirs.
+        """
+        for name in ['finished', 'graduated', 'unfinished', 'logs', 'deleted']:
+            path = os.path.join(self.storage, name)
+            if os.path.exists(path):
+                continue
+            os.mkdir(path)
+        return
+
+    @property
+    def deleted_path(self):
+        """Get the path for deleted student data.
+        """
+        return os.path.join(self.storage, 'deleted')
+
+    @property
+    def graduated_path(self):
+        """Get the path for deleted graduated student data.
+        """
+        return os.path.join(self.storage, 'graduated')
+
+    def getPendingFiles(self, sort='name'):
+        """Get a list of files stored in `storage`.
+
+        Files are sorted by basename.
+        """
+        result = []
+        if not os.path.exists(self.storage):
+            return result
+        for filename in sorted(os.listdir(self.storage)):
+            fullpath = os.path.join(self.storage, filename)
+            if not os.path.isfile(fullpath):
+                continue
+            if not (filename.endswith('.csv') or filename.endswith('.gz')):
+                continue
+            result.append(DataCenterFile(fullpath))
+        if sort == 'date':
+            # sort results in newest-first order...
+            result = sorted(result, key=lambda x: x.getTimeStamp(),
+                            reverse=True)
+        return result
+
+    def getFinishedFiles(self):
+        """Get a list of files stored in `finished` subfolder of `storage`.
+
+        Files are unsorted.
+        """
+        result = []
+        finished_dir = os.path.join(self.storage, 'finished')
+        if not os.path.exists(finished_dir):
+            return result
+        mtime = lambda f: os.stat(os.path.join(finished_dir, f)).st_mtime
+        finished_files = [f for f in
+            sorted(os.listdir(finished_dir), key=mtime, reverse=True)
+            if fnmatch.fnmatch(f, '*.finished.csv')]
+        for filename in finished_files[:self.max_files]:
+            fullpath = os.path.join(finished_dir, filename)
+            if not os.path.isfile(fullpath):
+                continue
+            if not filename.endswith('.csv'):
+                continue
+            result.append(DataCenterFile(fullpath, 'finished'))
+        return result
+
+    def getLogFiles(self, exclude_backups=True):
+        """Get the files from logs/ subdir. Files are sorted by name.
+
+        By default backup logs ('app.log.1', etc.) and payments.log
+        are excluded.
+        """
+        result = []
+        logdir = os.path.join(self.storage, 'logs')
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        for name in sorted(os.listdir(logdir)):
+            if not os.path.isfile(os.path.join(logdir, name)):
+                continue
+            if name == 'payments.log':
+                continue
+            if exclude_backups == True and RE_LOGFILE_BACKUP_NAME.match(name):
+                continue
+            result.append(
+                LogFile(os.path.join(self.storage, 'logs', name)))
+        return result
+
+    def setStoragePath(self, path, move=False, overwrite=False):
+        """Set the path where to store files.
+        """
+        path = os.path.abspath(path)
+        not_copied = []
+        if not os.path.exists(path):
+            raise ValueError('The path given does not exist: %s' % path)
+        if move is True:
+            not_copied = copy_filesystem_tree(self.storage, path,
+                                            overwrite=overwrite)
+        self.storage = path
+        self._createSubDirs()
+        grok.notify(DataCenterStorageMovedEvent(self))
+        return not_copied
+
+    def _moveFile(self, source, dest):
+        """Move file source to dest preserving ctime, mtime, etc.
+        """
+        if not os.path.exists(source):
+            self.logger.warn('No such source path: %s' % source)
+            return
+        if source == dest:
+            return
+        shutil.copyfile(source, dest)
+        shutil.copystat(source, dest)
+        os.unlink(source)
+
+    def _appendCSVFile(self, source, dest):
+        """Append data from CSV file `source` to data from CSV file `dest`.
+
+        The `source` file is deleted afterwards.
+        """
+        if not os.path.exists(dest):
+            return self._moveFile(source, dest)
+        if not os.path.exists(source):
+            self.logger.warn('No such source path: %s' % source)
+            return
+        if source == dest:
+            return
+        result_path = merge_csv_files(dest, source)
+        os.chmod(result_path, 0664)
+        self._moveFile(result_path, dest)
+        os.unlink(source)
+
+    def distProcessedFiles(self, successful, source_path, finished_file,
+                           pending_file, mode='create', move_orig=True):
+        """Put processed files into final locations.
+
+        ``successful`` is a boolean that tells, whether processing was
+        successful.
+
+        ``source_path``: path to file that was processed.
+
+        ``finished_file``, ``pending_file``: paths to the respective
+        generated .pending and .finished file. The .pending file path
+        may be ``None``.
+
+        If finished file is placed in a location outside the local
+        storage dir, the complete directory is removed
+        afterwards. Regular processors should put their stuff in
+        dedicated temporary dirs.
+
+        See datacenter.txt for more info about how this works.
+        """
+        basename = os.path.basename(source_path)
+        pending_name = basename
+        pending = False
+        finished_dir = os.path.join(self.storage, 'finished')
+        unfinished_dir = os.path.join(self.storage, 'unfinished')
+
+        if basename.endswith('.pending.csv'):
+            maybe_basename = "%s.csv" % basename.rsplit('.', 3)[0]
+            maybe_src = os.path.join(unfinished_dir, maybe_basename)
+            if os.path.isfile(maybe_src):
+                basename = maybe_basename
+                pending = True
+
+        base, ext = os.path.splitext(basename)
+        finished_name = "%s.%s.finished%s" % (base, mode, ext)
+        if not pending:
+            pending_name = "%s.%s.pending%s" % (base, mode, ext)
+
+        # Put .pending and .finished file into respective places...
+        pending_dest = os.path.join(self.storage, pending_name)
+        finished_dest = os.path.join(finished_dir, finished_name)
+        self._appendCSVFile(finished_file, finished_dest)
+        if pending_file is not None:
+            self._moveFile(pending_file, pending_dest)
+
+        # Put source file into final location...
+        finished_dest = os.path.join(finished_dir, basename)
+        unfinished_dest = os.path.join(unfinished_dir, basename)
+        if successful and not pending:
+            self._moveFile(source_path, finished_dest)
+        elif successful and pending:
+            self._moveFile(unfinished_dest, finished_dest)
+            os.unlink(source_path)
+        elif not successful and not pending:
+            self._moveFile(source_path, unfinished_dest)
+
+        # If finished and pending-file were created in a location
+        # outside datacenter storage, we remove it.
+        maybe_temp_dir = os.path.dirname(finished_file)
+        if os.path.commonprefix(
+            [self.storage, maybe_temp_dir]) != self.storage:
+            shutil.rmtree(maybe_temp_dir)
+        return
+
+    def _logfiles(self, basename):
+        """Get sorted logfiles starting with `basename`.
+        """
+        def numerical_suffix(name):
+            # return numerical suffix in `name` as number or 0.
+            suffix = name.rsplit('.', 1)[-1]
+            try:
+                return int(suffix)
+            except ValueError:
+                return 0
+            pass
+        files = [basename,]
+        for name in os.listdir(os.path.join(self.storage, 'logs')):
+            if RE_LOGFILE_BACKUP_NAME.match(name):
+                if name.rsplit('.', 1)[0] == basename:
+                    files.append(name)
+        return sorted(files, key=numerical_suffix, reverse=True)
+
+    def queryLogfiles(self, basename, query=None, limit=0, start=0):
+        """Search `query` in all logfiles starting with `basename`.
+
+        Returns an iterator of those lines in logfiles starting with
+        `basename` that match `query`. If you want the result as a
+        list, simply list() the iterator.
+
+        All logfiles with name `basename` and maybe some numerical
+        extension ('.1', '.2', ...) are searched for the `query` term
+        in correct chronological order. So, if you ask for a basename 'app.log',
+        then any file named 'app2.log', 'app.log.1', 'app.log',
+        etc. will be searched in that order.
+
+        The `query` is expected to be a string containing a regular
+        expression.
+
+        If `limit` is set to some numerical value, at most this number
+        of lines is returned.
+
+        With `start` you can give the number of first matched line to
+        return. `start` is zero-based, i.e. the first match has number
+        0, the scond one 1, etc.
+
+        Together with `limit` this allows some basic
+        batching. Please keep in mind that batching might give
+        unpredictable results, when logfiles change between two
+        requests. This is not a problem when only one file is searched
+        and changes include only appending new log messages.
+
+        Matches are found per line only (no multiline matches).
+
+        Result lines are returned as unicode instances decoded from
+        UTF-8 encoding. This means that logfiles must provide UTF-8
+        encoding for umlauts etc. if these should be rendered
+        properly. The returned unicode lines can be fed to page
+        templates even if they contain non-ASCII characters.
+
+        This method raises ValueError if some basic condition is not
+        met, for instance if the given query string is not a valid
+        regular expression.
+
+        Please note, that this exception will happen not before you
+        really fetch a result line.
+        """
+        try:
+            re_query = re.compile(query)
+        except:
+            raise ValueError('Invalid query string: %s' % query)
+
+        basename = basename.replace('/../', '')
+        files = self._logfiles(basename)
+
+        # Search the log files
+        num = 0
+        for name in files:
+            path = os.path.join(self.storage, 'logs', name)
+            if not os.path.isfile(path):
+                continue
+            for line in codecs.open(path, 'rb', 'utf-8'):
+                if not re_query.search(line):
+                    continue
+                num += 1
+                if (num - 1) < start:
+                    continue
+                yield line
+
+                if limit and (num - limit >= start):
+                    raise StopIteration
+        pass
+
+class DataCenterFile(object):
+    """A description of a file stored in data center.
+    """
+    grok.implements(IDataCenterFile)
+
+    def __init__(self, context, folder_name=''):
+        self.context = context
+        self.name = os.path.basename(self.context)
+        self.rel_path = os.path.join(folder_name, self.name)
+        self.size = self.getSize()
+        self.uploaddate = self.getDate()
+        self.lines = self.getLinesNumber()
+
+    def getDate(self):
+        """Get a human readable datetime representation.
+        """
+        date = datetime.fromtimestamp(os.path.getmtime(self.context))
+        return date.strftime("%Y-%m-%d %H:%M:%S")
+
+    def getTimeStamp(self):
+        """Get a (machine readable) timestamp.
+        """
+        return os.path.getmtime(self.context)
+
+    def getSize(self):
+        """Get a human readable file size.
+        """
+        bytesize = os.path.getsize(self.context)
+        size = "%s bytes" % bytesize
+        units = ['kb', 'MB', 'GB']
+        for power, unit in reversed(list(enumerate(units))):
+            power += 1
+            if bytesize >= 1024 ** power:
+                size = "%.2f %s" % (bytesize/(1024.0**power), unit)
+                break
+        return size
+
+    def getLinesNumber(self):
+        """Get number of lines.
+        """
+        num = 0
+        if not self.context.title().lower().endswith('csv'):
+            return 1
+        for line in open(self.context, 'rb'):
+            num += 1
+        return num
+
+class LogFile(DataCenterFile):
+    """A description of a log file.
+    """
+    def __init__(self, context):
+        super(LogFile, self).__init__(context)
+        self._markers = dict()
+        self._parsed = False
+        self.userid = self.getUserId()
+        self.mode = self.getMode()
+        self.stats = self.getStats()
+        self.source = self.getSourcePath()
+
+    def _parseFile(self, maxline=10):
+        """Find markers in a file.
+        """
+        if self._parsed:
+            return
+        for line in codecs.open(self.context, 'rb', 'utf-8'):
+            line = line.strip()
+            if not ':' in line:
+                continue
+            name, text = line.split(':', 1)
+            self._markers[name.lower()] = text
+        self._parsed = True
+        return
+
+    def _getMarker(self, marker):
+        marker = marker.lower()
+        if not self._parsed:
+            self._parseFile()
+        if marker in self._markers.keys():
+            return self._markers[marker]
+
+    def getUserId(self):
+        return self._getMarker('user') or '<UNKNOWN>'
+
+    def getMode(self):
+        return self._getMarker('mode') or '<NOT SET>'
+
+    def getStats(self):
+        return self._getMarker('processed') or '<Info not avail.>'
+
+    def getSourcePath(self):
+        return self._getMarker('source') or None
+
+
+class DataCenterStorageMovedEvent(ObjectEvent):
+    """An event fired, when datacenter storage moves.
+    """
+    grok.implements(IDataCenterStorageMovedEvent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/accesscode_browser.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/accesscode_browser.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/accesscode_browser.txt	(revision 16045)
@@ -0,0 +1,445 @@
+UI Components for Access Codes
+******************************
+
+.. module:: waeup.kofa.accesscodes.browser
+
+Here we visit the access-code related parts of a Kofa site using
+a virtual browser.
+
+Preliminaries
+=============
+
+Before we can do anything, we have to create a university site:
+
+    >>> from zope.testbrowser.testing import Browser
+    >>> browser = Browser()
+    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+    >>> browser.handleErrors = False
+    >>> root = getRootFolder()
+
+    >>> from waeup.kofa.app import University
+    >>> u = University()
+    >>> root['myuniversity'] = u
+
+We set a new datacenter storage path:
+
+  >>> import os
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Data Center').click()
+  >>> browser.getLink('Edit settings').click()
+  >>> pathsetting = browser.getControl(name='newpath')
+
+  >>> cwd = os.getcwd()
+  >>> uploadpath = os.path.join(cwd, 'ac_testfiles')
+  >>> os.mkdir(uploadpath)
+  >>> pathsetting.value = uploadpath
+  >>> browser.getControl(name='save').click()
+
+We remove any existing 'accesscodes' dir from datacenter dir:
+
+  >>> import shutil
+  >>> if os.path.exists(os.path.join(uploadpath, 'accesscodes')):
+  ...   shutil.rmtree(os.path.join(uploadpath, 'accesscodes'))
+
+
+Access Code Management Screen
+=============================
+
+For users that have the right to manage access-code related stuff, the
+home page of a `University` instance provides a link to the
+access-code management screen. In the beginning, there are already
+three empty batches available which will be filled by online payments:
+
+    >>> browser.open('http://localhost/myuniversity')
+    >>> browser.getLink('Access Codes').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...<h1 class="kofa-content-label">Access Code Batches</h1>
+    ...
+    ... The following batches are available:
+    ...CLR...
+    ...HOS...
+    ...SFE...
+    ...
+
+Adding Batches
+==============
+
+We can add a batch of access-codes using a button displayed in the
+action bar:
+
+    >>> browser.getLink('Add Access Code Batch').click()
+
+The add screen shows a form where we have to enter a prefix, the
+number of access codes to be generated and the costs for each card.
+
+    >>> browser.getControl(name='form.prefix').value = 'APP'
+    >>> browser.getControl(name='form.entry_num').value = '5'
+    >>> browser.getControl(name='form.cost').value = '12.12'
+
+If we click 'cancel' afterwards, the whole process will be cancelled
+and we'll be redirected to the management screen:
+
+    >>> browser.getControl('Cancel').click()
+    >>> browser.url
+    'http://localhost/myuniversity/accesscodes'
+
+    >>> 'Batch creation cancelled' in browser.contents
+    True
+
+Now let's try again and this time we finish the procedure by clicking
+'Create batch' in the form:
+
+    >>> browser.getLink('Add Access Code Batch').click()
+    >>> browser.getControl(name='form.prefix').value = 'APP'
+    >>> browser.getControl(name='form.entry_num').value = '5'
+    >>> browser.getControl(name='form.cost').value = '12.12'
+    >>> browser.getControl('Create batch').click()
+
+We're also redirected to the management screen, with a notice about
+the freshly created batch:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...<h1 class="kofa-content-label">Access Code Batches</h1>
+    ...
+    ... The following batches are available:
+    ...APP
+    ...-
+    ...1
+    ...
+    ...5
+    ...
+    ...0
+    ...
+    ...12.12...
+    ...zope.mgr...
+    ...
+
+which means: there exists a batch named ``APP-1`` with 5 entries of
+which 0 have been devalidated, each one costs ``12.12`` and the batch
+was created by ``zope.mgr``.
+
+We create a second batch to see whether searching and related stuff
+works:
+
+    >>> browser.getLink('Add Access Code Batch').click()
+    >>> browser.getControl(name='form.prefix').value = 'APP'
+    >>> browser.getControl(name='form.entry_num').value = '5'
+    >>> browser.getControl(name='form.cost').value = '10.12'
+    >>> browser.getControl('Create batch').click()
+
+And a third one that can be deleted afterwards:
+
+    >>> browser.getLink('Add Access Code Batch').click()
+    >>> browser.getControl(name='form.prefix').value = 'BLA'
+    >>> browser.getControl(name='form.entry_num').value = '3'
+    >>> browser.getControl(name='form.cost').value = '19.12'
+    >>> browser.getControl('Create batch').click()
+
+
+Creating Archive Files
+======================
+
+Once a batch is created, we can archive it. To do so we have to tick
+the respective checkbox and click on 'Archive':
+
+    >>> ctrl = browser.getControl(name='batches')
+    >>> ctrl.getControl(value='APP-2').selected = True
+    >>> browser.getControl(name='archive').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    <div ...>Archived APP-2 (APP-2_archive-...-zope.mgr.csv)</div>
+    ...
+
+If we do not select a batch and try to archive or delete, the system
+will complain:
+
+    >>> browser.getControl(name='archive').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    <div ...>No batch selected.</div>
+    ...
+
+Deleting Batches
+================
+
+We can delete batches. They are automatically archived when doing so:
+
+    >>> ctrl = browser.getControl(name='batches')
+    >>> ctrl.getControl(value='BLA-1').selected = True
+    >>> browser.getControl('Archive and delete').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...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 Access Code 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`` archive batch file over to the ``imports`` directory:
+
+    >>> ac_storage = os.path.join(uploadpath, 'accesscodes')
+    >>> logfile2 = os.path.join(ac_storage, 
+    ...                         sorted(os.listdir(ac_storage))[-2])
+    >>> filename = os.path.basename(logfile2)
+    >>> filename
+    'BLA-1_archive...-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 Access Code 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 Access Code 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_archive...-zope.mgr.csv...
+    ...
+
+If we try to reimport an existing batch, that won't work:
+
+    >>> browser.getLink('Reimport Access Code 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_archive...-zope.mgr.csv...
+    ...
+
+Searching Access Codes
+======================
+
+We can search for access codes already created.
+
+First we pick some really existing access codes to look for:
+
+    >>> codes = getRootFolder()['myuniversity']['accesscodes']
+    >>> app_1_codes = codes[u'APP-1']
+    >>> app_2_codes = codes[u'APP-2']
+    >>> bla_1_codes = codes[u'BLA-1']
+
+    >>> browser.open('http://localhost/myuniversity/accesscodes/search')
+    >>> ctrl = browser.getControl(name='searchtype')
+    >>> ctrl.getControl(value='code').selected = True
+    >>> browser.getControl(name='searchterm').value = app_1_codes.keys()[0]
+    >>> browser.getControl(name='search').click()
+
+The first access code in the ``APP-1`` batch is displayed:
+
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    ...
+      value="APP-1..." /></td>
+      <td>...</td>
+      <td>APP-1-...</td>
+      <td>initialized</td>
+      <td>... - initialized by Manager</td>
+    ...
+
+We can also search for batch serials (the number of an access code
+inside its batch). Looking for number ``1`` will display first access
+code of each batch ('APP-1', 'APP-2', and 'BLA-1')
+
+    >>> browser.open('http://localhost/myuniversity/accesscodes/search')
+    >>> ctrl = browser.getControl(name='searchtype')
+    >>> ctrl.getControl(value='batch_serial').selected = True
+    >>> browser.getControl(name='searchterm').value = '1'
+    >>> browser.getControl(name='search').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    ...
+      value="APP-1..." /></td>
+      <td>1</td>
+      <td>APP-1-...</td>
+      <td>initialized</td>
+      <td>... - initialized by Manager</td>
+    ...
+
+Searching for non-integer values does not result in an exception:
+
+    >>> ctrl = browser.getControl(name='searchtype')
+    >>> ctrl.getControl(value='batch_serial').selected = True
+    >>> browser.getControl(name='searchterm').value = 'xyz'
+    >>> browser.getControl(name='search').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+
+And we can search for text in history messages of access
+codes. Looking for the string ``initialized`` we get nearly all
+entries:
+
+    >>> browser.open('http://localhost/myuniversity/accesscodes/search')
+    >>> ctrl = browser.getControl(name='searchtype')
+    >>> ctrl.getControl(value='history').selected = True
+    >>> browser.getControl(name='searchterm').value = 'initialized'
+    >>> browser.getControl(name='search').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    ...
+      value="APP-1-<10-DIGITS>" /></td>
+      <td>1</td>
+      <td>APP-1-<10-DIGITS></td>
+      <td>initialized</td>
+      <td>... - initialized by Manager</td>
+    ...
+
+Enabling and Disabling Found Access Codes
+=========================================
+
+If a search is successfull, we can enable or disable the found access
+codes:
+
+    >>> browser.open('http://localhost/myuniversity/accesscodes/search')
+    >>> ctrl = browser.getControl(name='searchtype')
+    >>> ctrl.getControl(value='history').selected = True
+    >>> browser.getControl(name='searchterm').value = 'initialized'
+    >>> browser.getControl(name='search').click()
+
+This lists all access codes. We now tick one to disable it and click
+on the appropriate button:
+
+    >>> entries = browser.getControl(name='entries')
+    >>> entries.controls[0].selected = True
+    >>> browser.getControl('Disable ACs').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    ...APP-1-<10-DIGITS> disabled.</div>
+    ...
+
+We cannot disable the same entry again:
+
+    >>> entries = browser.getControl(name='entries')
+    >>> entries.controls[0].selected = True
+    >>> browser.getControl('Disable ACs').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    <div ...>APP-1-<10-DIGITS>: Disable transition not allowed.</div>
+    ...
+
+But we can reenable the same entry:
+
+    >>> entries = browser.getControl(name='entries')
+    >>> entries.controls[0].selected = True
+    >>> browser.getControl('Enable ACs').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    ...APP-1-<10-DIGITS> (re-)enabled.</div>
+    ...
+
+Enabling already enabled items gives a warning:
+
+    >>> entries = browser.getControl(name='entries')
+    >>> entries.controls[0].selected = True
+    >>> browser.getControl('Enable ACs').click()
+    >>> print browser.contents
+    <!DOCTYPE html ...
+    <div ...>APP-1-<10-DIGITS>: Re-enable transition not allowed.</div>
+    ...
+
+Log- and Archive Files
+======================
+
+We store log- and archive-files inside the storage managed by the
+local datacenter. Access-code related files are placed inside an
+``accesscode`` subdir:
+
+    >>> ac_storage = os.path.join(uploadpath, 'accesscodes')
+
+Log files for access-code batches
+---------------------------------
+
+Whenever a batch is created, there is also a log file with all entries
+created:
+
+    >>> sorted(os.listdir(ac_storage))
+    ['APP-1-...-zope.mgr.csv', 'APP-2-...-zope.mgr.csv', ...]
+
+Each logfile name contains the prefix, batch number, date of creation
+and userid of creator.
+
+    >>> logfile1 = os.path.join(ac_storage, 
+    ...                         sorted(os.listdir(ac_storage))[0])
+    >>> print open(logfile1, 'rb').read()
+    "serial","ac","cost"
+    "APP","1","12.12"
+    "0","APP-1-<10-DIGITS>"
+    "1","APP-1-<10-DIGITS>"
+    "2","APP-1-<10-DIGITS>"
+    "3","APP-1-<10-DIGITS>"
+    "4","APP-1-<10-DIGITS>"
+
+
+Archive files
+-------------
+
+We created an archive file above. An archive file name consists of the
+batch prefix, batch number, the string ``_archive``, creation datetime of
+the archive file and userid of batch creator:
+
+    >>> sorted(os.listdir(ac_storage))
+    [..., '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,
+    ...                             sorted(os.listdir(ac_storage))[-2])
+    >>> print open(archive_file, 'rb').read()
+    "prefix","serial","ac","state","history","cost","owner"
+    "BLA","19.12","1","3"
+    "BLA","0","BLA-1-<10-DIGITS>","initialized","...","19.12",""
+    "BLA","1","BLA-1-<10-DIGITS>","initialized","...","19.12",""
+    "BLA","2","BLA-1-<10-DIGITS>","initialized","...","19.12",""
+
+Clean up:
+
+    >>> shutil.rmtree(uploadpath)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/app.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/app.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/app.txt	(revision 16045)
@@ -0,0 +1,100 @@
+Central Components
+******************
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+.. module:: waeup.kofa.app
+
+
+Creating `University` instances
+===============================
+
+As :class:`University` instances make use of the Zope Component
+Architecture (ZCA), we have to setup the basic component registries,
+before we can create instances. We do this by simply grokking the
+whole :mod:`waeup` package. This way all interfaces, utilities and adapters
+defined in the package are looked up and registered with the global
+registries (this is done by the testlayer automatically).
+
+Now we can import the :class:`University` class and create an
+instance:
+
+  >>> from waeup.kofa.app import University
+  >>> myuniversity = University()
+  >>> myuniversity
+  <waeup.kofa.app.University object at 0x...>
+
+Instances of `University` comply with the interface
+`waeup.kofa.interfaces.IUniversity`:
+
+  >>> from zope.interface.verify import verifyClass
+  >>> from waeup.kofa.interfaces import IUniversity
+  >>> verifyClass(IUniversity, University)
+  True
+
+A freshly created instance provides the attributes promised by the
+interface:
+
+  >>> from waeup.kofa.app import University
+  >>> myuniversity = University()
+  >>> myuniversity['configuration'].name
+  u'Sample University'
+
+  >>> myuniversity['faculties']
+  <waeup.kofa.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+  >>> myuniversity['students']
+  <waeup.kofa.students.container.StudentsContainer object at 0x...>
+
+  >>> myuniversity['users']
+  <waeup.kofa.userscontainer.UsersContainer object at 0x...>
+
+  >>> myuniversity['datacenter']
+  <waeup.kofa.datacenter.DataCenter object at 0x...>
+
+  >>> myuniversity['configuration']
+  <waeup.kofa.configuration.ConfigurationContainer object at 0x...>
+
+Kofa plugins
+============
+
+waeup.kofa provides an API to 'plugin' components. Things that should
+be setup at creation time of a Kofa application can indicate
+that by providing a utility providing IKofaPlugin.
+
+The plugins are looked up by an created app, which then will call the
+``setup()`` method of each plugin.
+
+   >>> from waeup.kofa.interfaces import IKofaPluggable
+   >>> from zope.component import getAdapters, getUtilitiesFor
+   >>> sorted(list(getUtilitiesFor(IKofaPluggable)))
+   [(u'academics', <waeup.kofa.university.facultiescontainer.AcademicsPlugin ...)]
+
+
+We can provide a new plugin like this:
+
+   >>> import grok
+   >>> from waeup.kofa.interfaces import IKofaPluggable
+   >>> class MyPlugin(grok.GlobalUtility):
+   ...   grok.implements(IKofaPluggable)
+   ...   def setup(self, site, name, logger):
+   ...     print "Setup was called for"
+   ...     print site
+   ...   def update(self, site, name, logger):
+   ...     pass
+
+When we register the plugin
+
+   >>> grok.testing.grok_component('MyPlugin', MyPlugin)
+   True
+
+and setup a new Kofa instance, we will get a message:
+
+   >>> from waeup.kofa.app import University
+   >>> site = University()
+   Setup was called for
+   <waeup.kofa.app.University object at 0x...>
+
+Apparently the plugin can do with the University object whatever it
+likes. That's what plugins are for.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/authentication.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/authentication.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/authentication.txt	(revision 16045)
@@ -0,0 +1,212 @@
+Authentication
+**************
+
+We need to protect most pieces of our portals from unauthenticated
+access.
+
+Therefore users have to login to access main functionality and they
+are able to log out afterwards.
+
+Before we can check access we have to create an app:
+
+  >>> from zope.component.hooks import setSite # only needed in tests
+  >>> from waeup.kofa.app import University
+  >>> root = getRootFolder()
+  >>> u = University()
+  >>> root['app'] = u
+  >>> setSite(root['app'])                     # only needed in tests
+
+To make sure, we can 'watch' pages, we first have to initialize our
+test browser:
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+  >>> browser.handleErrors = False
+
+Creating officers
+=================
+
+Before we can login, we have to provide a user (``principal`` in Zope
+terms) with a password (and optional a title or description):
+
+  >>> root['app']['users'].addUser('bob', 'bobSecret1',
+  ...                           title='Bob', description='A sample user')
+
+We can also add complete `Account` objects. An `Account` stores the
+user credentials and some metadata persistently:
+
+  >>> from waeup.kofa.authentication import Account
+  >>> alice = Account('alice', 'alicesecret',roles=['waeup.ManageDataCenter'])
+  >>> root['app']['users'].addAccount(alice)
+
+See ``userscontainer.txt`` for details about the UsersContainer we use here.
+
+Officers and local roles
+========================
+
+Accounts also hold infos about local roles assigned to a user. In the
+beginning, users have the local owner role of their own account object:
+
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.kofa.authentication.Account object at 0x...>]}
+
+User  automatically get the global AcademicsOfficer role:
+
+  >>> alice.getSiteRolesForPrincipal()
+  ['waeup.ManageDataCenter', 'waeup.AcademicsOfficer']
+
+We can tell an account, that Alice got some role for another object:
+
+  >>> chalet = object()
+  >>> root['app']['chalet'] = chalet
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=True)
+
+Now Alice is the Big Boss:
+
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When we do not want Alice to be the Big Boss we can tell that too:
+
+  >>> alice.notifyLocalRoleChanged(chalet, 'BigBoss', granted=False)
+  >>> alice.getLocalRoles()
+  {'waeup.local.Owner': [<waeup.kofa.authentication.Account object at 0x...>]}
+
+We can also use events to trigger such actions. This is recommended
+because we do not neccessarily know where Alice lives:
+
+  >>> from waeup.kofa.authentication import LocalRoleSetEvent
+  >>> from zope.event import notify
+  >>> notify(LocalRoleSetEvent(chalet, 'BigBoss', 'alice',
+  ...                          granted=True))
+  >>> alice.getLocalRoles()
+  {'BigBoss': [<object object at 0x...>]}
+
+When objects are deleted, local roles are also deleted
+semi-magically. This happens through event subscribers listening to
+IObjectRemovedEvents. The latters are naturally only fired when ZODB
+stored objects are removed. Furthermore this subscriber reads the
+internal local roles table.
+
+We create a faculty and grant Bob a local role:
+
+   >>> from zope.securitypolicy.interfaces import IPrincipalRoleManager
+   >>> from waeup.kofa.university.faculty import Faculty
+   >>> faculty = Faculty()
+   >>> root['app']['bobs_fac'] = faculty
+   >>> role_manager = IPrincipalRoleManager(faculty)
+   >>> role_manager.assignRoleToPrincipal(
+   ...    'waeup.PortalManager', 'bob')
+
+We notify the machinery about that fact:
+
+   >>> notify(LocalRoleSetEvent(faculty, 'waeup.PortalManager', 'bob',
+   ...                          granted=True))
+   >>> bob = root['app']['users']['bob']
+   >>> bob.getLocalRoles()
+   {'waeup.PortalManager': [<waeup.kofa...Faculty object at 0x...>]}
+
+When we delete the faculty from ZODB, also Bobs roles are modified:
+
+   >>> del root['app']['bobs_fac']
+   >>> bob.getLocalRoles()
+   {'waeup.local.Owner': [<waeup.kofa.authentication.Account object at 0x...>]}
+
+If one notifies the machinery of a local role removal for an object
+that cannot have local roles, this will cause no trouble:
+
+   >>> mycontext = None #object()
+   >>> notify(LocalRoleSetEvent(
+   ...   mycontext, 'waeup.PortalManager', 'bob', granted=False
+   ... )) is None
+   True
+
+When an account get deleted, also the local roles of the owner get
+removed. Let's setup a local role for `alice`:
+
+   >>> faculty = Faculty()
+   >>> root['app']['alice_fac'] = faculty
+   >>> role_manager = IPrincipalRoleManager(faculty)
+   >>> role_manager.assignRoleToPrincipal(
+   ...    'waeup.PortalManager', 'alice')
+   >>> notify(LocalRoleSetEvent(faculty, 'waeup.PortalManager', 'alice',
+   ...                          granted=True))
+
+The local role is set now:
+
+   >>> from zope.securitypolicy.interfaces import IPrincipalRoleMap
+   >>> IPrincipalRoleMap(faculty).getPrincipalsAndRoles()
+   [('waeup.PortalManager', 'alice', PermissionSetting: Allow)]
+
+But when we delete Alices account from ZODB:
+
+   >>> del root['app']['users']['alice']
+   >>> IPrincipalRoleMap(faculty).getPrincipalsAndRoles()
+   []
+
+the local role has gone and also the site roles have been removed:
+
+   >>> prm = IPrincipalRoleManager(root['app'])
+   >>> [x[0] for x in prm.getRolesForPrincipal('alice')
+   ...      if x[0].startswith('waeup.')]
+   []
+
+
+Logging in via side bar
+=======================
+
+We can access the front page without restrictions:
+
+  >>> browser.open('http://localhost/app')
+  >>> print browser.headers['Status']
+  200 Ok
+
+We have to go to one of the login pages first:
+
+  >>> browser.open('http://localhost/app')
+  >>> browser.getLink('Login').click()
+  >>> print browser.headers['Status']
+  200 Ok
+
+There is a login form on tis page:
+
+  >>> 'form.login' in browser.contents
+  True
+
+  >>> 'form.logout' in browser.contents
+  False
+
+We use this form:
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'invalidpw'
+  >>> browser.getControl('Login').click()
+  >>> 'You entered invalid credentials' in browser.contents
+  True
+
+  >>> browser.getControl(name='form.login').value = 'bob'
+  >>> browser.getControl(name='form.password').value = 'bobSecret1'
+  >>> browser.getControl('Login').click()
+
+Now the login form is gone. Instead we have the opportunity to logout:
+
+  >>> 'form.login' in browser.contents
+  False
+
+  >>> logout = browser.getLink('Logout')
+  >>> logout
+  <Link text='Logout' url='http://localhost/app/@@logout'>
+
+The user title is also displayed in the sidebar:
+
+  >>> 'Bob' in browser.contents
+  True
+
+We can also log out afterwards:
+
+  >>> logout.click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Login
+  ...
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing.txt	(revision 16045)
@@ -0,0 +1,554 @@
+Batch Processing
+****************
+
+Batch processing is much more than pure data import.
+
+Overview
+========
+
+Basically, it means processing CSV files in order to mass-create,
+mass-remove, or mass-update data.
+
+So you can feed CSV files to processors, that are part of
+the batch-processing mechanism.
+
+Processors
+----------
+
+Each CSV file processor
+
+* accepts a single data type identified by an interface.
+
+* knows about the places inside a site (University) where to store,
+  remove or update the data.
+
+* can check headers before processing data.
+
+* supports the mode 'create', 'update', 'remove'.
+
+* creates log entries (optional)
+
+* creates csv files containing successful and not-successful processed
+  data respectively.
+
+Output
+------
+
+The results of processing are written to loggers, if a logger was
+given. Beside this new CSV files are created during processing:
+
+* a pending CSV file, containing datasets that could not be processed
+
+* a finished CSV file, containing datasets successfully processed.
+
+The pending file is not created if everything works fine. The
+respective path returned in that case is ``None``.
+
+The pending file (if created) is a CSV file that contains the failed
+rows appended by a column ``--ERRROR--`` in which the reasons for
+processing failures are listed.
+
+The complete paths of these files are returned. They will be in a
+temporary directory created only for this purpose. It is the caller's
+responsibility to remove the temporay directories afterwards (the
+datacenters distProcessedFiles() method takes care for that).
+
+It looks like this::
+ 
+     -----+      +---------+
+    /     |      |         |              +------+
+   | .csv +----->|Batch-   |              |      |
+   |      |      |processor+----changes-->| ZODB |
+   |  +------+   |         |              |      |
+   +--|      |   |         +              +------+
+      | Mode +-->|         |                 -------+
+      |      |   |         +----outputs-+-> /       |
+      |  +----+->+---------+            |  |.pending|
+      +--|Log |  ^                      |  |        |
+         +----+  |                      |  +--------+
+           +-----++                     v
+           |Inter-|                  ----------+
+           |face  |                 /          |
+           +------+                | .finished |
+                                   |           |
+                                   +-----------+
+
+
+Creating a Batch Processor
+==========================
+
+We create an own batch processor for an own datatype. This datatype
+must be based on an interface that the batcher can use for converting
+data.
+
+Founding Stoneville
+-------------------
+
+We start with the interface:
+
+    >>> from zope.interface import Interface
+    >>> from zope import schema
+    >>> class ICave(Interface):
+    ...   """A cave."""
+    ...   name = schema.TextLine(
+    ...     title = u'Cave name',
+    ...     default = u'Unnamed',
+    ...     required = True)
+    ...   dinoports = schema.Int(
+    ...     title = u'Number of DinoPorts (tm)',
+    ...     required = False,
+    ...     default = 1)
+    ...   owner = schema.TextLine(
+    ...     title = u'Owner name',
+    ...     required = True,
+    ...     missing_value = 'Fred Estates Inc.')
+    ...   taxpayer = schema.Bool(
+    ...     title = u'Payes taxes',
+    ...     required = True,
+    ...     default = False)
+
+Now a class that implements this interface:
+
+    >>> import grok
+    >>> class Cave(object):
+    ...   grok.implements(ICave)
+    ...   def __init__(self, name=u'Unnamed', dinoports=2,
+    ...                owner='Fred Estates Inc.', taxpayer=False):
+    ...     self.name = name
+    ...     self.dinoports = 2
+    ...     self.owner = owner
+    ...     self.taxpayer = taxpayer
+
+We also provide a factory for caves. Strictly speaking, this not
+necessary but makes the batch processor we create afterwards, better
+understandable.
+
+    >>> from zope.component import getGlobalSiteManager
+    >>> from zope.component.factory import Factory
+    >>> from zope.component.interfaces import IFactory
+    >>> gsm = getGlobalSiteManager()
+    >>> cave_maker = Factory(Cave, 'A cave', 'Buy caves here!')
+    >>> gsm.registerUtility(cave_maker, IFactory, 'Lovely Cave')
+
+Now we can create caves using a factory:
+
+    >>> from zope.component import createObject
+    >>> createObject('Lovely Cave')
+    <Cave object at 0x...>
+
+This is nice, but we still lack a place, where we can place all the
+lovely caves we want to sell.
+
+Furthermore, as a replacement for a real site, we define a place where
+all caves can be stored: Stoneville! This is a lovely place for
+upperclass cavemen (which are the only ones that can afford more than
+one dinoport).
+
+We found Stoneville:
+
+    >>> stoneville = dict()
+
+Everything in place.
+
+Now, to improve local health conditions, imagine we want to populate
+Stoneville with lots of new happy dino-hunting natives that slept on
+the bare ground in former times and had no idea of
+bathrooms. Disgusting, isn't it?
+
+Lots of cavemen need lots of caves.
+
+Of course we can do something like:
+
+    >>> cave1 = createObject('Lovely Cave')
+    >>> cave1.name = "Fred's home"
+    >>> cave1.owner = "Fred"
+    >>> stoneville[cave1.name] = cave1
+
+and Stoneville has exactly
+
+    >>> len(stoneville)
+    1
+
+inhabitant. But we don't want to do this for hundreds or thousands of
+citizens-to-be, do we?
+
+It is much easier to create a simple CSV list, where we put in all the
+data and let a batch processor do the job.
+
+The list is already here:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner,taxpayer
+    ... Barneys Home,2,Barney,1
+    ... Wilmas Asylum,1,Wilma,1
+    ... Freds Dinoburgers,10,Fred,0
+    ... Joeys Drive-in,110,Joey,0
+    ... """)
+
+All we need, is a batch processor now.
+
+    >>> from waeup.kofa.utils.batching import BatchProcessor
+    >>> from waeup.kofa.interfaces import IGNORE_MARKER
+    >>> class CaveProcessor(BatchProcessor):
+    ...   util_name = 'caveprocessor'
+    ...   grok.name(util_name)
+    ...   name = 'Cave Processor'
+    ...   iface = ICave
+    ...   location_fields = ['name']
+    ...   factory_name = 'Lovely Cave'
+    ...
+    ...   def parentsExist(self, row, site):
+    ...     return True
+    ...
+    ...   def getParent(self, row, site):
+    ...     return stoneville
+    ...
+    ...   def entryExists(self, row, site):
+    ...     return row['name'] in stoneville.keys()
+    ...
+    ...   def getEntry(self, row, site):
+    ...     if not self.entryExists(row, site):
+    ...       return None
+    ...     return stoneville[row['name']]
+    ...
+    ...   def delEntry(self, row, site):
+    ...     del stoneville[row['name']]
+    ...
+    ...   def addEntry(self, obj, row, site):
+    ...     stoneville[row['name']] = obj
+    ...
+    ...   def updateEntry(self, obj, row, site, filename):
+    ...     # This is not strictly necessary, as the default
+    ...     # updateEntry method does exactly the same
+    ...     for key, value in row.items():
+    ...       if value != IGNORE_MARKER:
+    ...         setattr(obj, key, value)
+
+If we also want the results being logged, we must provide a logger
+(this is optional):
+
+    >>> import logging
+    >>> logger = logging.getLogger('stoneville')
+    >>> logger.setLevel(logging.DEBUG)
+    >>> logger.propagate = False
+    >>> handler = logging.FileHandler('stoneville.log', 'w')
+    >>> logger.addHandler(handler)
+
+Create the fellows:
+
+    >>> processor = CaveProcessor()
+    >>> result = processor.doImport('newcomers.csv',
+    ...                   ['name', 'dinoports', 'owner', 'taxpayer'],
+    ...                    mode='create', user='Bob', logger=logger)
+    >>> result
+    (4, 0, '/.../newcomers.finished.csv', None)
+
+The result means: four entries were processed and no warnings
+occured. Furthermore we get filepath to a CSV file with successfully
+processed entries and a filepath to a CSV file with erraneous entries.
+As everything went well, the latter is ``None``. Let's check:
+
+    >>> sorted(stoneville.keys())
+    [u'Barneys Home', ..., u'Wilmas Asylum']
+
+The values of the Cave instances have correct type:
+
+    >>> barney = stoneville['Barneys Home']
+    >>> barney.dinoports
+    2
+
+which is a number, not a string.
+
+Apparently, when calling the processor, we gave some more info than
+only the CSV filepath. What does it all mean?
+
+While the first argument is the path to the CSV file, we also have to
+give an ordered list of headernames. These replace the header field
+names that are actually in the file. This way we can override faulty
+headers.
+
+The ``mode`` paramter tells what kind of operation we want to perform:
+``create``, ``update``, or ``remove`` data.
+
+The ``user`` parameter finally is optional and only used for logging.
+
+We can, by the way, see the results of our run in a logfile if we
+provided a logger during the call:
+
+    >>> print open('stoneville.log').read()
+    processed: newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+
+
+We cleanup the temporay dir created by doImport():
+
+    >>> import shutil
+    >>> import os
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+As we can see, the processing was successful. Otherwise, all problems
+could be read here as we can see, if we do the same operation again:
+
+    >>> result = processor.doImport('newcomers.csv',
+    ...                   ['name', 'dinoports', 'owner', 'taxpayer'],
+    ...                    mode='create', user='Bob', logger=logger)
+    >>> result
+    (4, 4, '/.../newcomers.finished.csv', '/.../newcomers.pending.csv')
+
+This time we also get a path to a .pending file.
+
+The log file will tell us this in more detail:
+
+    >>> print open('stoneville.log').read()
+    processed: newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+    processed: newcomers.csv, create mode, 4 lines (0 successful/ 4 failed), ... s (... s/item)
+
+
+This time a new file was created, which keeps all the rows we could not
+process and an additional column with error messages:
+
+    >>> print open(result[3]).read()
+    owner,name,taxpayer,dinoports,--ERRORS--
+    Barney,Barneys Home,1,2,This object already exists.
+    Wilma,Wilmas Asylum,1,1,This object already exists.
+    Fred,Freds Dinoburgers,0,10,This object already exists.
+    Joey,Joeys Drive-in,0,110,This object already exists.
+
+This way we can correct the faulty entries and afterwards retry without
+having the already processed rows in the way.
+
+We also notice, that the values of the taxpayer column are returned as
+in the input file. There we wrote '1' for ``True`` and '0' for
+``False`` (which is accepted by the converters).
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can also tell to ignore some cols from input by passing
+``--IGNORE--`` as col name:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             '--IGNORE--', '--IGNORE--'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (4, 0, '...', None)
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+If something goes wrong during processing, the respective --IGNORE--
+cols won't be populated  in the resulting pending file:
+
+    >>> result = processor.doImport('newcomers.csv', ['name', 'dinoports',
+    ...                             '--IGNORE--', '--IGNORE--'],
+    ...                    mode='create', user='Bob')
+    >>> result
+    (4, 4, '...', '...')
+
+    >>> print open(result[3], 'rb').read()
+    name,dinoports,--ERRORS--
+    Barneys Home,2,This object already exists.
+    Wilmas Asylum,1,This object already exists.
+    Freds Dinoburgers,10,This object already exists.
+    Joeys Drive-in,110,This object already exists.
+
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+Updating Entries
+----------------
+
+To update entries, we just call the batchprocessor in a different
+mode:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (4, 0, '...', None)
+
+Now we want to tell, that Wilma got an extra port for her second dino:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... Wilmas Asylum,2,Wilma
+    ... """)
+
+    >>> wilma = stoneville['Wilmas Asylum']
+    >>> wilma.dinoports
+    1
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We start the processor:
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                    'dinoports', 'owner'], mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma = stoneville['Wilmas Asylum']
+    >>> wilma.dinoports
+    2
+
+Wilma's number of dinoports raised.
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+If we try to update an unexisting entry, an error occurs:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... NOT-WILMAS-ASYLUM,2,Wilma
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 1, '/.../newcomers.finished.csv', '/.../newcomers.pending.csv')
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+    
+Also invalid values will be spotted:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... Wilmas Asylum,NOT-A-NUMBER,Wilma
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 1, '...', '...')
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can also update only some cols, leaving some out. We skip the
+'dinoports' column in the next run:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,owner
+    ... Wilmas Asylum,Barney
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name', 'owner'],
+    ...                             mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.owner
+    u'Barney'
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+We can however, not leave out the 'location field' ('name' in our
+case), as this one tells us which entry to update:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... 2,Wilma
+    ... """)
+
+    >>> processor.doImport('newcomers.csv', ['dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    Traceback (most recent call last):
+    ...
+    FatalCSVError: Need at least columns 'name' for import!
+
+This time we get even an exception!
+
+Generally, empty strings are considered as ``None``:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","","Wilma"
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.dinoports
+    2
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+We can tell to set dinoports to ``None`` although this is not a
+number, as we declared the field not required in the interface:
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","XXX","Wilma"
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob', ignore_empty=False)
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.dinoports is None
+    True
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+Removing Entries
+----------------
+
+In 'remove' mode we can delete entries. Here validity of values in
+non-location fields doesn't matter because those fields are ignored.
+
+    >>> open('newcomers.csv', 'wb').write(
+    ... """name,dinoports,owner
+    ... "Wilmas Asylum","ILLEGAL-NUMBER",""
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='remove', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> sorted(stoneville.keys())
+    [u'Barneys Home', "Fred's home", u'Freds Dinoburgers', u'Joeys Drive-in']
+
+Oops! Wilma is gone.
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+
+Clean up:
+
+    >>> import os
+    >>> os.unlink('newcomers.csv')
+    >>> os.unlink('stoneville.log')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing_browser.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing_browser.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/batchprocessing_browser.txt	(revision 16045)
@@ -0,0 +1,685 @@
+Batch Processing via Browser
+****************************
+
+Preliminaries:
+
+We define a function that looks up a form with several submit buttons
+for the one with a given value (this functionality is missing in
+zope.testbrowser):
+
+    >>> def lookup_submit_value(name, value, browser):
+    ...   """Find a button with a certain value."""
+    ...   for num in range(0, 100):
+    ...     try:
+    ...       button = browser.getControl(name=name, index=num)
+    ...       if button.value.endswith(value):
+    ...         return button
+    ...     except IndexError:
+    ...       break
+    ...   return None
+
+Create a site:
+
+    >>> from waeup.kofa.app import University
+    >>> getRootFolder()['app'] = University()
+    >>> from zope.component.hooks import setSite
+    >>> setSite(getRootFolder()['app'])
+
+Create a datacenter storage path:
+
+    >>> import os
+    >>> import tempfile
+    >>> dc_path = tempfile.mkdtemp()
+
+Log in:
+
+    >>> from zope.testbrowser.testing import Browser
+    >>> browser = Browser()
+    >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+    >>> browser.handleErrors = False
+
+Set datacenter path and deselect moving old data:
+
+    >>> browser.open('http://localhost/app')
+    >>> browser.getLink('Data Center').click()
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = dc_path
+    >>> browser.getControl(name='move').value = False
+    >>> browser.getControl(name='save').click()
+
+Set non-usable datacenter path:
+
+    >>> browser.getLink('Edit settings').click()
+    >>> browser.getControl(name='newpath').value = '/'
+    >>> browser.getControl(name='save').click()
+    >>> 'Given storage path cannot be used.' in browser.contents
+    True
+    >>> browser.getControl('Back to Data Center').click()
+
+
+Batch Processing Faculties
+==========================
+
+Go to datacenter page:
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for faculties (extended ascii values are accepted):
+
+    >>> open('faculties.csv', 'wb').write(
+    ... """code,title,title_prefix
+    ... FAC1,Faculty 1,faculty
+    ... FAC2,Faculty 2,institute
+    ... FAC3,Fäcülty 3,school
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('faculties.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'faculties.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'faculties_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.displayOptions
+    ['AccessCodeBatch Processor',
+    'AccessCode Processor',
+    'ApplicantOnlinePayment Processor',
+    'Applicant Processor',
+    'ApplicantsContainer Processor',
+    'Bed Processor (update only)',
+    'CertificateCourse Processor',
+    'Certificate Processor',
+    'Course Processor',
+    'CourseTicket Processor',
+    'Department Processor',
+    'Faculty Processor',
+    'Hostel Processor',
+    'Public HTML Document Processor',
+    'StudentOnlinePayment Processor',
+    'Public PDF Document Processor',
+    'Public REST Document Processor',
+    'Student Processor',
+    'StudentStudyCourse Processor (update only)',
+    'StudentStudyLevel Processor',
+    'User Processor',
+    'Verdict Processor (special processor, update only)']
+
+    >>> importerselect.getControl('Faculty Processor').selected = True
+
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.options
+    ['create', 'update', 'remove']
+
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 3 rows...
+    ...Batch processing finished...
+    ...File:...faculties_zope.mgr.csv...
+
+We can grep the entries generated in logfile:
+
+    >>> browser.open('http://localhost/app/datacenter/logs')
+    >>> browser.getControl('Show', index=0).click()
+    >>> print browser.contents
+    <!DOCTYPE ...
+    ...<h1 class="kofa-content-label">Logfile datacenter.log</h1>...
+
+    >>> browser.getControl(name='query').value = "zope.mgr"
+    >>> browser.getControl('Search').click()
+    >>> 'zope.mgr - processed' in browser.contents
+    True
+
+
+Batch Processing Departments
+============================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for departments:
+
+    >>> open('departments.csv', 'wb').write(
+    ... """code,faculty_code,title,title_prefix
+    ... DEP1,FAC1,Department 1,department
+    ... DEP2,FAC2,Department 2,centre
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('departments.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'departments.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'departments_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('Department Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...departments_zope.mgr.csv...
+
+Batch Processing Courses
+========================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for courses:
+
+    >>> open('courses.csv', 'wb').write(
+    ... """code,faculty_code,department_code,title,level,passmark,credits,semester
+    ... CRS1,FAC1,DEP1,Course 1,100,40,2,1
+    ... CRS2,FAC1,DEP1,Course 2,100,40,2,2
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('courses.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'courses.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'courses_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('Course Processor', index=1).selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...courses_zope.mgr.csv...
+
+Batch Processing Certificates
+=============================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for certificates:
+
+    >>> open('certificates.csv', 'wb').write(
+    ... """code,faculty_code,department_code,title,study_mode,start_level,end_level,application_category
+    ... CERT1,FAC1,DEP1,Certificate 1,pg_ft,999,999,basic
+    ... CERT2,FAC1,DEP1,Certificate 2,ug_ft,200,300,cest
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('certificates.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'certificates.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'certificates_zope.mgr.csv', browser)
+    >>> button.click()
+
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('Certificate Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...certificates_zope.mgr.csv...
+
+Batch Processing Certificate Courses
+====================================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for certificate courses:
+
+    >>> open('mycertcourses.csv', 'wb').write(
+    ... """course,faculty_code,department_code,certificate_code,level,mandatory
+    ... CRS1,FAC1,DEP1,CERT1,100,True
+    ... CRS2,FAC1,DEP1,CERT1,100,True
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('mycertcourses.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'mycertcourses.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'mycertcourses_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('CertificateCourse Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...mycertcourses_zope.mgr.csv...
+
+Batch Processing Users
+======================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for users:
+
+    >>> open('users.csv', 'wb').write(
+    ... """name,title,public_name,email,phone,roles
+    ... uli,Uli Fouquet,Chief Developer,uli@abc.de,+49-234-567,[]
+    ... henrik,Henrik Bettermann,Admin,henrik@abc.de,+49-234-567,"['waeup.PortalManager', 'waeup.ImportManager']"
+    ... anne,Anne Palina,,anne@abc.de,+49-234-567,"['waeup.Nonsense']"
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('users.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'users.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'users_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('User Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import').disabled
+    False
+
+    >>> browser.getControl('Perform import').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...users_zope.mgr.csv...
+
+User henrik has got the global roles:
+
+   >>> henrik = getRootFolder()['app']['users']['henrik']
+   >>> henrik.roles
+   ['waeup.PortalManager', 'waeup.AcademicsOfficer', 'waeup.ImportManager']
+
+Pending Files
+=============
+
+When an error occurs during an import, two files are generated: a CSV
+file with finished files and a CSV file with pending data. Both are
+stored in the appropriate subdirectories in datacenter. We try to
+create faculties, from which one already exists.
+
+Go to datacenter page:
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for faculties:
+
+    >>> open('newfaculties.csv', 'wb').write(
+    ... """code,title,title_prefix
+    ... FAC1,Faculty 1,faculty
+    ... FAC4,Faculty 4,school
+    ... FAC 5,Faculty 5,faculty
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload data').click()
+    >>> filecontents = cStringIO.StringIO(
+    ...   open('newfaculties.csv', 'rb').read())
+    >>> filewidget = browser.getControl(name='uploadfile:file')
+    >>> filewidget.add_file(filecontents, 'text/plain', 'newfaculties.csv')
+    >>> browser.getControl(name='SUBMIT').click()
+
+Since we now have a user with waeup.ImportManager role, an email has been sent:
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    ...All import managers have been notified by email...
+    ...
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newfaculties_zope.mgr.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('Faculty Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3: Fix headerlines
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+Two lines could not be imported:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Processing of 2 rows failed...
+    ...Successfully processed 1 rows...
+    ...
+
+Now there are two files as a result in datacenter storage's root and
+``finished`` dirs:
+
+    >>> pending_file = dc_path + '/newfaculties_zope.mgr.create.pending.csv'
+    >>> print open(pending_file).read()
+    title_prefix,code,title,--ERRORS--
+    faculty,FAC1,Faculty 1,This object already exists.
+    faculty,FAC 5,Faculty 5,code: Invalid input
+
+    >>> finished_file = dc_path + '/finished/newfaculties_zope.mgr.create.finished.csv'
+    >>> print open(finished_file).read()
+    title_prefix,code,title
+    school,FAC4,Faculty 4
+
+The finished-file contains the dataset we could import, while the
+pending file contains the dataset that failed, appended by an error
+message.
+
+
+Fixing the Pending File
+-----------------------
+
+We 'edit' the pending file (setting code to ``FAC5`` and title
+appropriately, and removing the --ERROR-- column) and finish the
+import this way:
+
+    >>> open(dc_path + '/newfaculties_zope.mgr.create.pending.csv', 'wb').write(
+    ... """title_prefix,--IGNORE--,code,title
+    ... faculty,,FAC5,Faculty 5
+    ... """)
+
+Step 1: start batch processing:
+
+    >>> browser.open('http://localhost/app/datacenter')
+    >>> browser.getLink('Process data').click()
+    >>> browser.getLink('Switch maintenance mode').click()
+    >>> button = lookup_submit_value(
+    ...   'select', 'newfaculties_zope.mgr.create.pending.csv', browser)
+    >>> button.click()
+
+Step 2: select a processor and mode:
+
+    >>> importerselect = browser.getControl(name='importer')
+    >>> importerselect.getControl('Faculty Processor').selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3').click()
+
+Step 3/4: Fix headerlines and import:
+
+As there should be no problem with the headers, we can immediately
+perfom the import:
+
+    >>> browser.getControl('Perform import').click()
+
+This time everything should work:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Successfully processed 1 rows...
+    ...
+
+Oh no, we forgot Anne Palina. Her user record was not imported because
+she has a non-existent role:
+
+    >>> sorted(os.listdir(dc_path))
+    ['deleted', 'finished', 'graduated', 'logs', 'unfinished', 'users_zope.mgr.create.pending.csv']
+
+    >>> os.listdir(dc_path + '/unfinished')
+    ['users_zope.mgr.csv']
+
+    >>> pending_file = dc_path + '/users_zope.mgr.create.pending.csv'
+    >>> print open(pending_file).read()
+    name,roles,title,public_name,phone,email,--ERRORS--
+    anne,['waeup.Nonsense'],Anne Palina,<IGNORE>,+49-234-567,anne@abc.de,roles: invalid role
+
+There are many finished-files:
+
+    >>> sorted(os.listdir(dc_path + '/finished'))
+    ['certificates_zope.mgr.create.finished.csv', ...,
+    'users_zope.mgr.create.finished.csv']
+
+Processed (finished) Files
+==========================
+
+    >>> browser.open('http://localhost/app/datacenter/processed')
+    >>> 'download?filename=finished/certificates_zope.mgr.create.finished.csv' in browser.contents
+    True
+
+Log Files
+=========
+
+    >>> browser.open('http://localhost/app/datacenter/logs')
+    >>> 'datacenter.log' in browser.contents
+    True
+    >>> browser.getControl('Show', index=0).click()
+    >>> browser.getControl('Back', index=0).click()
+    >>> browser.getControl('Back to Data Center').click()
+    >>> 'Storage path:' in browser.contents
+    True
+
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(dc_path)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/breadcrumbs.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/breadcrumbs.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/breadcrumbs.txt	(revision 16045)
@@ -0,0 +1,243 @@
+Breadcrumbs
+***********
+
+We use an adapter approach to get a flexible system of creating and
+retrieving breadcrumbs for certain views.
+
+
+How to get existing breadcrumbs
+===============================
+
+We create a university to check the breadcrumb functionality.
+
+    >>> from waeup.kofa.app import University
+    >>> root = getRootFolder()
+    >>> root['app'] = University()
+    >>> app = root['app']
+
+The quick way (if you have a viewname)
+--------------------------------------
+
+We can get an ordered list of breadcrumbs for a given context and
+viewname:
+
+    >>> from waeup.kofa.browser.breadcrumbs import getBreadcrumbList
+    >>> blist1 = getBreadcrumbList(app, 'index')
+    >>> blist1
+    [<waeup.kofa.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>]
+
+A slightly more extensive list for the datacenter:
+
+    >>> blist2 = getBreadcrumbList(app['datacenter'], 'index')
+    >>> from pprint import pprint
+    >>> pprint(blist2)
+    [<waeup.kofa.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>,
+     <waeup.kofa.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <waeup.kofa.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>]
+
+We get a breadcrumb for university, administration area and data
+center in that order.
+
+Each breadcrumb provides a ``title``, ``context``, and ``viewname``
+attribute, so that views and page templates can easily create markup
+out of it:
+
+     >>> pprint([(x.title) for x in blist2])
+     [u'Home', u'Administration', u'Data Center']
+
+     >>> pprint([(x.context, x.viewname) for x in blist2])
+     [(<waeup.kofa.app.University object at 0x...>, 'index'),
+      (<waeup.kofa.app.University object at 0x...>, 'administration'),
+      (<waeup.kofa.datacenter.DataCenter object at 0x...>, 'index')]
+
+The administration area breadcrumb might be a surprise, as there is no
+equivalent object in the ZODB. In fact the administration area is only
+a certain view (the 'administration' view) on the university object.
+
+We will show below, how you can define breadcrumbs this way.
+
+As a rule of thumb you should know, that every breadcrumb is not only
+attached for a certain kind of object, but for a tuple of context
+object and a viewname.
+
+This way we can make sure, that there are different breadcrumb lists
+generated for instance for the administration view of University
+instances and the index view. While the first should look something
+like::
+
+  Home -> Administration
+
+we expect for the latter only::
+
+  Home
+
+
+Another quick way (if you have a complete view)
+-----------------------------------------------
+
+Another quick approach for use in viewlets, views and similar
+contexts, where you usually have a view available but not a viewname,
+is to use ``getBreadcrumbListForView``.
+
+We create a view (a page in this case, but pages are views) on the
+user administration index page:
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.component import getMultiAdapter
+    >>> page = getMultiAdapter((app['users'], TestRequest()), name='index')
+
+Now we can get the breadcrumbs for this view:
+
+    >>> from waeup.kofa.browser.breadcrumbs import getBreadcrumbListForView
+    >>> blist3 = getBreadcrumbListForView(page)
+    >>> [x.title for x in blist3]
+    [u'Home', u'Administration', u'Officers']
+
+..note:: This works only for views created with grokcore.component
+         (i.e. Grok Views, Pages, etc.) but not for simple Zope3
+         views, as the viewname can not easily be extracted from the
+         view for the latter ones.
+
+
+Type-safe retrieval of breadcrumbs for a view
+---------------------------------------------
+
+If you want to make sure, that a view for which you want to get
+breadcrumbs is really a Grok view, then you can use the
+IBreadcrumbContainer adapter. This one adapts only grok views.
+
+The returned breadcrumb container supports iteration:
+
+    >>> from waeup.kofa.browser.interfaces import IBreadcrumbContainer
+    >>> mybccontainer = IBreadcrumbContainer(page)
+    >>> [x.title for x in mybccontainer]
+    [u'Home', u'Administration', u'Officers']
+
+It is, however, not a real Python list but only an iterable
+container. You can also get the real list of breadcrumbs:
+
+    >>> pprint(mybccontainer.getList())
+    [<...breadcrumbs.UniversityBreadcrumb object at 0x...>,
+     <...breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <...breadcrumbs.UsersContainerBreadcrumb object at 0x...>]
+
+
+
+The general details
+-------------------
+
+Now we can get breadcrumbs for contexts and view names. For example a
+breadcrumb for the 'index' view of our University object:
+
+    >>> from zope.component import getAdapter
+    >>> from waeup.kofa.browser.interfaces import IBreadcrumb
+    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
+    >>> b1
+    <waeup.kofa.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>
+
+Breadcrumb objects provide a title:
+
+    >>> b1.title
+    u'Home'
+
+a parent (None for the app object):
+
+    >>> b1.parent is None
+    True
+
+and a viewname:
+
+    >>> b1.viewname
+    'index'
+
+Using a context and a viewname, one can easily compute a complete URL
+in views, viewlets, etc.
+
+    >>> b2 = getAdapter(app['datacenter'], IBreadcrumb, 'index')
+    >>> b2
+    <waeup.kofa.browser.breadcrumbs.DataCenterBreadcrumb object at 0x...>
+
+    >>> b2.title
+    u'Data Center'
+
+    >>> b2.viewname
+    'index'
+
+As the data center is not root of the hierarchy, it provides a
+non-trivial parent:
+
+    >>> b2.parent
+    (<waeup.kofa.app.University object at 0x...>, 'administration')
+
+This result denotes a new context object (the University instance we
+created above) and a view name ('administration').
+
+Normally, the view name would be 'index', but as some administrative
+components should look to the user as if they were grouped in a common
+subarea of the root (the administration area), we can also set a
+different view than the default as target.
+
+When we like to receive the breadcrumb for this parent, we can do so
+as above:
+
+    >>> context, viewname = b2.parent
+    >>> b3 = getAdapter(context, IBreadcrumb, viewname)
+    >>> b3
+    <waeup.kofa...breadcrumbs.AdministrationBreadcrumb object at 0x...>
+
+As you can see, we get an AdministrationBreadcrumb, although the
+context object, for which the breadcrumb was created is also the
+University instance as above:
+
+    >>> b3.context is b1.context
+    True
+
+This administration area now has still a parent, the front-page:
+
+    >>> context, viewname = b3.parent
+    >>> context, viewname
+    (<waeup.kofa.app.University object at 0x...>, 'index')
+
+We create last breadcrumb:
+
+    >>> b4 = getAdapter(context, IBreadcrumb, viewname)
+
+This is finally the last piece, the breadcrumb which links to the
+front page. It has no parent:
+
+    >>> b4.parent is None
+    True
+
+
+How to create new breadcrumbs
+=============================
+
+Breadcrumbs are looked up as named adapters. This means, you must
+create a named adapter with the following specifications:
+
+ * Derive your breadcrumb class from ``Breadcrumb`` class. This will
+   save you lots of work.
+
+ * The adapter context (``grok.context``) must be the object type you
+   want to provide a breadcrumb for. So, if you want to write a
+   Breadcrumb adapter for objects providing ``IMyFunnyType``, write::
+
+     grok.context(IMyFunnyType)
+
+ * The adapter name (``grok.name``) must be the same as the name of
+   the view, for which the breadcrumb component should be
+   registered. 
+
+   If you want to write a breadcrumb component for the 'index' view of
+   some content type and derive your class from Breadcrumb, then you
+   can skip this step as 'index' is the default name.
+
+ * Set a ``title`` attribute to give the text that should be
+   displayed. Use something like::
+
+     @property
+     def title(self):
+       return self.some_computed_title()
+
+   if you want computed titles.
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/catalog.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/catalog.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/catalog.txt	(revision 16045)
@@ -0,0 +1,193 @@
+Cataloging Support
+******************
+
+.. module:: waeup.kofa.catalog
+
+Components that support cataloging and searching objects inside a
+Kofa site.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Getting a general query object
+==============================
+
+We can get a KofaQuery object by asking for an unnamed global utility
+implementing `hurry.query.interfaces.IQuery`:
+
+    >>> from hurry.query.interfaces import IQuery
+    >>> from zope.component import getUtility
+    >>> q = getUtility(IQuery)
+    >>> q
+    <waeup.kofa.catalog.KofaQuery object at 0x...>
+
+This query can get 'subqueries' and delivers the objects found or
+their ids. To show this we have to setup a catalog with some entries.
+
+
+Setting up a catalog and feeding it
+===================================
+
+    >>> from zope.catalog.interfaces import ICatalog
+    >>> from zope.catalog.catalog import Catalog
+    >>> mycat = Catalog()
+
+We register this catalog with the component architechture as a utility
+named 'mycatalog':
+
+    >>> from zope.component import provideUtility
+    >>> provideUtility(mycat, ICatalog, 'mycatalog')
+
+We setup a special content type whose instances we will catalog later:
+
+    >>> from zope.interface import Interface, Attribute, implements
+    >>> from zope.container.contained import Contained
+    >>> class IMammoth(Interface):
+    ...   name = Attribute('name')
+    ...   age = Attribute('age')
+
+    >>> class Mammoth(Contained):
+    ...   implements(IMammoth)
+    ...   def __init__(self, name, age):
+    ...     self.name = name
+    ...     self.age = age
+    ...   def __cmp__(self, other):
+    ...     return cmp(self.name, other.name)
+
+By including the __cmp__ method we make sure search results can be
+stably sorted.
+
+We also setup a `zope.intid.interfaces.IIntIds` utility. This is not
+necessary for plain catalogs, but when we want to use KofaQuery (or
+`hurry.query.query.Query` objects), as to get a unique mapping from
+objects (stored in ZODB) to integer numbers (stored in catalogs),
+these query objects lookup a global IIntIds utiliy:
+
+    >>> from zope import interface
+    >>> import zope.intid.interfaces
+    >>> class DummyIntId(object):
+    ...     interface.implements(zope.intid.interfaces.IIntIds)
+    ...     MARKER = '__dummy_int_id__'
+    ...     def __init__(self):
+    ...         self.counter = 0
+    ...         self.data = {}
+    ...     def register(self, obj):
+    ...         intid = getattr(obj, self.MARKER, None)
+    ...         if intid is None:
+    ...             setattr(obj, self.MARKER, self.counter)
+    ...             self.data[self.counter] = obj
+    ...             intid = self.counter
+    ...             self.counter += 1
+    ...         return intid
+    ...     def getObject(self, intid):
+    ...         return self.data[intid]
+    ...     def __iter__(self):
+    ...         return iter(self.data)
+    >>> intid = DummyIntId()
+    >>> from zope.component import provideUtility
+    >>> provideUtility(intid, zope.intid.interfaces.IIntIds)
+
+Now we can catalog some mammoths. Here we create a herd and catalog
+each item of it:
+
+    >>> from zope.catalog.field import FieldIndex
+    >>> mycat['mammoth_name'] = FieldIndex('name', IMammoth)
+    >>> mycat['mammoth_age'] = FieldIndex('age', IMammoth)
+
+    >>> herd = [
+    ...   Mammoth(name='Fred', age=33),
+    ...   Mammoth(name='Hank', age=30),
+    ...   Mammoth(name='Wilma', age=28),
+    ... ]
+
+    >>> for mammoth in herd:
+    ...   mycat.index_doc(intid.register(mammoth), mammoth)
+
+
+Searching for result sets
+=========================
+
+Finally we can perform queries:
+
+    >>> from hurry.query import Eq
+    >>> from zope.component import getUtility
+    >>> subquery1 = Eq(('mycatalog', 'mammoth_name'), 'Fred')
+
+The latter means: search for objects whose name is ``'Fred'`` in the
+``mammoth_name`` index of a catalog registered as a utility named
+``mycatalog``.
+
+    >>> from hurry.query import Between
+    >>> subquery2 = Between(('mycatalog', 'mammoth_age'), 30, 33)
+
+This means: ask for objects cataloged in an index named 'mammoth_age',
+whose cataloged value is between 30 and 33 (including this values).
+
+    >>> r1 = q.apply(subquery2)
+    >>> r1
+    IFSet([0, 1])
+
+Using ``apply()`` above, we get a set of values stored in an
+``IFBTree``:
+
+    >>> type(r1)
+    <type 'BTrees.IFBTree.IFSet'>
+
+``IFBTree`` objects implement a rather efficient integer to float
+mapping where also integers are allowed as values. For each object
+found (i.e. mammoths whose age is between 30 and 33), we get the
+number of its entry.
+
+To get the real object, we can use intids here, because we setup an
+appropriate IIntIds utility before:
+
+    >>> [intid.getObject(x).name for x in r1]
+    ['Fred', 'Hank']
+
+We can (and should) also use the `searchResults()` method explained
+below to do that.
+
+Retrieving BTree sets can, however, make sense, if you want to know
+only the number of results for a particular query or whether there are
+results at all in a more efficient way:
+
+    >>> len(r1)
+    2
+
+Searching for objects
+=====================
+
+Very often we don't want to know the catalog-internal 'ids' of
+searched objects but the objects themselves.
+
+This can be done by using the ``searchResults`` method of
+``KofaQuery``:
+
+    >>> r2 = q.searchResults(subquery1)
+    >>> r2
+    <zope.catalog.catalog.ResultSet instance at 0x...>
+
+    >>> list(r2)
+    [<Mammoth object at 0x...>]
+
+We got one result item, we can immediately ask for further infos. To
+access a result item by its index number, we have to turn the
+ResultSet into an ordinary list before:
+
+    >>> entry = list(r2)[0]
+    >>> entry.name, entry.age
+    ('Fred', 33)
+
+We can also use ``subquery2`` as above:
+
+    >>> r3 = q.searchResults(subquery2)
+    >>> [(x.name, x.age) for x in r3]
+    [('Fred', 33), ('Hank', 30)]
+
+or use both queries at once:
+
+    >>> r4 = q.searchResults(subquery1 & subquery2)
+    >>> [(x.name, x.age) for x in r4]
+    [('Fred', 33)]
+
+which will give us, of course, the same result set as with subquery1.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certcourse.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certcourse.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certcourse.txt	(revision 16045)
@@ -0,0 +1,221 @@
+.. _removecertificatecourses:
+
+Persistence of Certificate Courses
+==================================
+
+If a certificate course requires a certain course and this is course
+is deleted, also the referring certificate course is deleted.
+
+We setup a data structure that reflects typical usage. It looks like
+this::
+
+    Department-Instance
+    |
+    +---> courses
+    |        |
+    |        +--------------------> Course-Instance
+    |                                        ^
+    +---> certificates                       |
+             |                               |
+             +-----> Certificate-Instance    |
+                        |                    |
+                        +------> Certificate-Course
+
+The Certifcate Course here refers to a Course instance.
+
+In Python we build such a structure like this (from top to bottom):
+
+    >>> from zope.component import createObject
+    >>> mydept = createObject('waeup.Department')
+
+In real world use this data will be stored in a ZODB. We setup our own
+litte ZODB backend (which is easy!):
+
+    >>> from ZODB import FileStorage, DB
+    >>> dbpath = 'tinyData.fs'
+    >>> class TinyZODB(object):
+    ...   def __init__(self, path=dbpath):
+    ...     self.storage = FileStorage.FileStorage(path)
+    ...     self.db = DB(self.storage)
+    ...     self.connection = self.db.open()
+    ...     self.dbroot = self.connection.root()
+    ...   def close(self):
+    ...     self.connection.close()
+    ...     self.db.close()
+    ...     self.storage.close()
+
+Now we can use this ZODB as backend database and store our data
+structure:
+
+    >>> import transaction
+    >>> db = TinyZODB()
+    >>> dbroot = db.dbroot
+    >>> dbroot['mydept'] = mydept
+    >>> mycourse = createObject('waeup.Course')
+    >>> mycourse.code = 'MYCOURSE'
+    >>> mydept.courses.addCourse(mycourse)
+    >>> mycert = createObject('waeup.Certificate')
+    >>> mycert.code = 'MYCERT'
+    >>> mydept.certificates.addCertificate(mycert)
+    >>> mycert.addCertCourse(mycourse)
+
+    >>> transaction.commit()
+
+The data is now stored in the ZODB. We can close the DB, reopen it
+later and the data will still be there:
+
+    >>> db.close()
+    >>> newdb = TinyZODB()
+    >>> newdbroot = newdb.dbroot
+    >>> list(newdbroot)
+    ['mydept']
+
+The certificate course we stored in the certificate is indeed a
+referrer of the course, not a copy of it:
+
+    >>> course = newdbroot['mydept'].courses['MYCOURSE']
+    >>> certcourse = newdbroot['mydept'].certificates['MYCERT']['MYCOURSE_100']
+    >>> certcourse.course is course
+    True
+
+So, we can be sure that modifications to the course are immediately
+reflected in the certcourse.
+
+We can also be sure, that when removing courses or referrers of them,
+other data will be handled in a way we expect. That is,
+
+* when we remove a course, we want all referrers to it be removed as
+  well
+
+* when we remove a referrer of a course, nothing else should be
+  touched but only this very referrer.
+
+
+As correct deletion of referrer needs support of catalogs (we lookup
+catalogs to find referrers), we need a fully blown ZODB here,
+including an instance of :class:`waeup.kofa.app.University`, as this is our
+`ISite` object where the catalogs are stored.
+
+We create a complete university tree, starting with a
+:class:`University` instance and going down to courses, certificates
+and course certificates:
+
+    >>> from waeup.kofa.app import University
+    >>> root = getRootFolder()
+    >>> root['app'] = University()
+
+We have to set the site, to enable proper working of catalogs and
+`IObjectRemoved` handlers:
+
+    >>> from zope.app.component.hooks import setSite
+    >>> setSite(root['app'])
+
+    >>> from zope.component import createObject
+    >>> faculty = createObject('waeup.Faculty')
+    >>> faculty.code = 'FAC'
+    >>> root['app']['faculties'].addFaculty(faculty)
+    >>> fac = root['app']['faculties']['FAC']
+
+    >>> dept = createObject('waeup.Department')
+    >>> dept.code = 'DEPT'
+    >>> root['app']['faculties']['FAC'].addDepartment(dept)
+
+    >>> course = createObject('waeup.Course')
+    >>> course.code = 'CRS1'
+    >>> fac['DEPT'].courses.addCourse(course)
+
+Now let's add another department, which will also hold a course:
+
+    >>> dept2 = createObject('waeup.Department')
+    >>> dept2.code = 'DEPT2'
+    >>> root['app']['faculties']['FAC'].addDepartment(dept2)
+
+    >>> course = createObject('waeup.Course')
+    >>> course.code = 'CRS2'
+    >>> fac['DEPT2'].courses.addCourse(course)
+
+Finally, we add a certificate with referrers (certificate courses) to
+the created courses:
+
+    >>> cert = createObject('waeup.Certificate')
+    >>> cert.code = 'CERT'
+    >>> fac['DEPT'].certificates.addCertificate(cert)
+
+    >>> fac['DEPT'].certificates['CERT'].addCertCourse(
+    ...   fac['DEPT'].courses['CRS1'])
+
+    >>> fac['DEPT'].certificates['CERT'].addCertCourse(
+    ...   fac['DEPT2'].courses['CRS2'])
+
+We add the latter course again, but this time for the second level:
+
+    >>> fac['DEPT'].certificates['CERT'].addCertCourse(
+    ...   fac['DEPT2'].courses['CRS2'], level=200)
+
+So, we have three referrers in our certificate of two different
+courses stored somewhere in the application:
+
+    >>> list(fac['DEPT'].certificates['CERT'])
+    [u'CRS1_100', u'CRS2_100', u'CRS2_200']
+
+Both, the courses themselves and the referrers of them, are the same
+(not only equal):
+
+    >>> a = root['app']['faculties']['FAC']['DEPT']
+    >>> b1 = a.courses['CRS1']
+    >>> b2 = a.certificates['CERT']['CRS1_100'].course
+    >>> b1 is b2
+    True
+
+If we remove the course, also the select item in the certificate course
+will be gone:
+
+    >>> list(a.certificates['CERT'])
+    [u'CRS1_100', u'CRS2_100', u'CRS2_200']
+
+    >>> list(a.courses)
+    [u'CRS1']
+
+    >>> del root['app']['faculties']['FAC']['DEPT'].courses['CRS1']
+    >>> list(a.certificates['CERT'])
+    [u'CRS2_100', u'CRS2_200']
+
+    >>> list(a.courses)
+    []
+
+If we remove a referrer of a course, this will not touch the course
+nor other referrers:
+
+    >>> del fac['DEPT'].certificates['CERT']['CRS2_200']
+    >>> list(a.certificates['CERT'])
+    [u'CRS2_100']
+
+    >>> list(fac['DEPT2'].courses)
+    [u'CRS2']
+
+If a single course is registered for several levels in a single
+certificate, this will not block things if the course is removed
+(regression test):
+
+We add the same course as a reference for level 200 and level 300:
+
+    >>> fac['DEPT'].certificates['CERT'].addCertCourse(
+    ...   fac['DEPT2'].courses['CRS2'], level=200)
+    >>> fac['DEPT'].certificates['CERT'].addCertCourse(
+    ...   fac['DEPT2'].courses['CRS2'], level=300)
+
+Deleting the course (thus also the refererrers) will work:
+
+    >>> del root['app']['faculties']['FAC']['DEPT2'].courses['CRS2']
+
+Now all references are gone:
+
+    >>> list(a.certificates['CERT'])
+    []
+
+and also the course itself vanished:
+
+    >>> list(fac['DEPT2'].courses)
+    []
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificate.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificate.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificate.txt	(revision 16045)
@@ -0,0 +1,259 @@
+Certificates and Certificate Courses
+************************************
+
+.. module:: waeup.kofa.university.certificate
+
+Components that represent and manage certificates.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Certificate`
+--------------------
+
+.. class:: Certificate([code=u'NA',[ title=u'Unnamed Certificate',[ study_mode=None,[ start_level=None,[ end_level=None,[ application_category=None,[ m_prefix=u'']]]]]]])
+
+   Create a certificate object with the given parameters.
+
+   .. attribute:: grok.implements(ICertificate)
+
+   All parameters are optional:
+
+     >>> from waeup.kofa.university.certificate import Certificate
+     >>> mycertificate = Certificate()
+
+   Certificates have the attributes required by the `ICertificate` interface:
+
+     >>> from waeup.kofa.university.interfaces import ICertificate
+     >>> ICertificate.providedBy(mycertificate)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(ICertificate, mycertificate)
+     True
+
+   Beside the attributes, certificates are containers for
+   certificate-courses (see :class:`CertificateCourse`). Each
+   certificate course can be accessed by the code of the course it wraps.
+
+   .. attribute:: title
+
+      Each certificate has a title:
+
+        >>> mycertificate.title
+        u'Unnamed Certificate'
+
+   .. attribute:: code
+
+      Each certificate holds a code, which might be a shortcut or
+      abbreviation of the real certificate name. By default the code
+      is ``NA`` (=not assigned):
+
+        >>> mycertificate.code
+        u'NA'
+
+
+   .. attribute:: study_mode
+
+      Each :class:`Certificate` instance has a study mode:
+
+        >>> print mycertificate.study_mode
+        None
+
+      .. XXX: This is not a proper description
+
+   .. attribute:: start_level
+
+      Each :class:`Certificate` instance has a start level:
+
+        >>> print mycertificate.start_level
+        None
+
+      .. XXX: This is not a proper description
+
+   .. attribute:: end_level
+
+      Each :class:`Certificate` instance has a end level:
+
+        >>> print mycertificate.end_level
+        None
+
+      .. XXX: This is not a proper description
+   
+
+   .. attribute:: application_category
+
+      Each :class:`Certificate` instance has an application category:
+
+        >>> print mycertificate.application_category
+        None
+
+      .. XXX: This is not a proper description
+
+   .. method:: addCertCourse(course[, level=100,[ mandatory=True]])
+
+      Add a certificate course. A course is an object implementing
+      :class:`waeup.kofa.interfaces.ICourse`.
+
+      Please don't be confused by the term 'certificate course'.
+      As course objects will normaly be  stored
+      in a department, the certificate course refers to
+      the 'real' course in the department container.
+
+   .. method:: delCertCourses(code)
+
+      Remove a certificate course from a certificate.
+
+      The certificate course must be given by its code number.
+
+:class:`CertificateCourse`
+--------------------------
+
+.. class:: CertificateCourse(course[, level=100[, mandatory=True]])
+
+   Create a certificate course.
+
+   A certificate course are held in
+   certificates and refer to an existing :class:`Course` instance held
+   elsewhere.
+
+   A certificate can require several certificate courses and one
+   certificate course can be required by several certificates.
+
+   .. attribute:: course
+
+      An instance of :class:`ICourse`.
+
+   .. attribute:: level
+
+      An integer telling the level to which this course applies.
+
+   .. attribute:: mandatory
+
+      A bool stating whether this course is required or optional to
+      get the certificate.
+
+
+
+Utilities
+=========
+
+:class:`CertificateFactory`
+---------------------------
+
+.. class:: CertificateFactory()
+
+   .. attribute:: grok.name(u'waeup.Certificate')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Certificate`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificate = createObject(u'waeup.Certificate')
+     >>> mycertificate
+     <waeup.kofa.university.certificate.Certificate object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.certificate import CertificateFactory
+     >>> verifyClass(IFactory, CertificateFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certificate_factory = CertificateFactory()
+     >>> certificate_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.certificate.Certificate>
+
+
+:class:`CertificateCourseFactory`
+---------------------------------
+
+.. class:: CertificateCourseFactory()
+
+   .. attribute:: grok.name(u'waeup.CertificateCourse')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CertificateCourse`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificatecourse = createObject(u'waeup.CertificateCourse')
+     >>> mycertificatecourse
+     <waeup.kofa.university.certificate.CertificateCourse object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.certificate import CertificateCourseFactory
+     >>> verifyClass(IFactory, CertificateCourseFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certcourse_factory = CertificateCourseFactory()
+     >>> certcourse_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.certificate.CertificateCourse>
+
+
+Examples
+========
+
+Certificates
+------------
+
+We can create certificates:
+
+    >>> from waeup.kofa.university.certificate import Certificate
+    >>> mycertificate = Certificate()
+    >>> mycertificate
+    <waeup.kofa.university.certificate.Certificate object at 0x...>
+
+Another way to create certificates is by asking for a factory called
+``waeup.Certificate``. This way we can create a factory without
+importing a class:
+
+    >>> from zope.component import createObject
+    >>> mycertificate = createObject(u'waeup.Certificate')
+    >>> mycertificate
+    <waeup.kofa.university.certificate.Certificate object at 0x...>
+
+Certificate Courses
+-------------------
+
+:class:`CertificateCourse` instances comply with the
+:class:`ICertificateCourse` interface:
+
+    >>> from waeup.kofa.university.interfaces import ICertificateCourse
+    >>> from waeup.kofa.university.certificate import CertificateCourse
+    >>> mycertcourse = CertificateCourse(None, 200, False)
+    >>> ICertificateCourse.providedBy(mycertcourse)
+    True
+
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(ICertificateCourse, mycertcourse)
+    True
+
+Also instances of :class:`CertificateCourse` can be created by asking
+the component architechture:
+
+    >>> from zope.component import createObject
+    >>> mycertcourse = createObject(u'waeup.CertificateCourse')
+    >>> mycertcourse
+    <waeup.kofa.university.certificate.CertificateCourse object at 0x...>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificatescontainer.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificatescontainer.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/certificatescontainer.txt	(revision 16045)
@@ -0,0 +1,147 @@
+Certificate Containers
+**********************
+
+.. module:: waeup.kofa.university.certificatescontainer
+
+Containers for certificates.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CertificatesContainer`
+------------------------------
+
+.. class:: CertificatesContainer()
+
+   Create a certificate container instance.
+
+   .. attribute:: grok.implements(ICertificatesContainer)
+
+   :class:`CertificatesContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.kofa.university.certificate.Certificate`.
+
+     >>> from waeup.kofa.university.certificatescontainer import CertificatesContainer
+     >>> mycontainer = CertificatesContainer()
+     >>> mycontainer
+     <waeup.kofa.university.certificatescontainer.CertificatesContainer object at 0x...>
+
+   Certificate containers provide `ICertificatesContainer`:
+
+     >>> from waeup.kofa.university.interfaces import ICertificatesContainer
+     >>> ICertificatesContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CertificatesContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICertificatesContainer, CertificatesContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CertificatesContainer` instances
+   can be used like dicts:
+
+     >>> list(mycontainer.keys())
+     []
+
+   .. method:: addCertificate(certificate)
+
+      Add a certificate.
+
+      *certificate* must be an instance of
+      :class:`waeup.kofa.university.certificate.Certificate`:
+
+        >>> from waeup.kofa.university.certificatescontainer import (
+        ...   CertificatesContainer)
+        >>> mycontainer = CertificatesContainer()
+        >>> mycontainer.addCertificate('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CertificatesContainers contain only ICertificate instances
+
+      The key of added items is the ``code`` attribute:
+
+        >>> from waeup.kofa.university.certificate import Certificate
+        >>> mycertificate = Certificate(code='CERTIFICATE1')
+        >>> mycontainer.addCertificate(mycertificate)
+        >>> list(mycontainer.keys())
+        [u'CERTIFICATE1']
+
+   .. method:: clear()
+
+      Remove all contained certificates:
+
+        >>> len(mycontainer.items())
+        1
+
+        >>> mycontainer.clear()
+        >>> len(mycontainer.items())
+        0
+
+Utilities
+=========
+
+:class:`CertificatesContainerFactory`
+-------------------------------------
+
+.. class:: CertificatesContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CertificatesContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CertificatesContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificatescontainer = createObject(u'waeup.CertificatesContainer')
+     >>> mycertificatescontainer
+     <waeup.kofa.university.certificatescontainer.CertificatesContainer object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.certificatescontainer import (
+     ...   CertificatesContainerFactory)
+     >>> verifyClass(IFactory, CertificatesContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certificatescontainer_factory = CertificatesContainerFactory()
+     >>> certificatescontainer_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.certificatescontainer.CertificatesContainer>
+
+
+Examples
+========
+
+Getting a certificate container
+-------------------------------
+
+We can easily create `CertificatesContainers`:
+
+    >>> from waeup.kofa.university.certificatescontainer import CertificatesContainer
+    >>> mycontainer = CertificatesContainer()
+
+
+Another way to get a certificate container -- without importing the
+class -- is via factories. We registered a factory for certificate
+containers under the name ``waeup.CertificatesContainer``. Now we can
+ask for an object by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CertificatesContainer')
+    <waeup.kofa.university.certificatescontainer.CertificatesContainer object at 0x...>
+
+This way we get a thing that implements ICertificatesContainer without
+imports or similar.
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/course.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/course.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/course.txt	(revision 16045)
@@ -0,0 +1,143 @@
+Courses
+*******
+
+.. module:: waeup.kofa.university.course
+
+Components that represent courses.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Content Classes (models and containers)
+=======================================
+
+:class:`Course`
+---------------
+
+.. class:: Course([code=u'NA'[, title=u'Unnamed Course'[, credits=0[, passmark=40[, semester=1]]]]]])
+
+   Create a course instance with the given parameters.
+
+   .. attribute:: grok.implements(ICourse)
+
+   All parameters are optional:
+
+     >>> from waeup.kofa.university.course import Course
+     >>> mycourse = Course()
+     >>> mycourse
+     <waeup.kofa.university.course.Course object at 0x...>
+
+   Course instances have the attributes required by the
+   :class:`waeup.kofa.interfaces.ICourse` interface:
+
+     >>> from waeup.kofa.university.interfaces import ICourse
+     >>> ICourse.providedBy(mycourse)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(ICourse, mycourse)
+     True
+
+   .. attribute:: title
+
+      Each course has a title:
+
+        >>> mycourse.title
+        u'Unnamed Course'
+
+   .. attribute:: code
+
+      Each course holds a code, which might be a shortcut or
+      abbreviation of the real course name. By default the code is
+      ``NA`` (=not assigned).
+
+      This value has to be unique if used in a :class:`Department`
+      instance as it serves as a key:
+
+        >>> mycourse.code
+        u'NA'
+
+   .. attribute:: passmark
+
+      Each course holdes a passmark, which is ``40`` by default:
+
+        >>> mycourse.passmark
+        40
+
+   .. attribute:: semester
+
+      Each course holds a semester attribute which is ``1`` by
+      default:
+
+        >>> mycourse.semester
+        1
+
+   .. attribute:: credits
+
+      Each course holds the number of credits that can be
+      earned. Default is ``0``.
+
+        >>> mycourse.credits
+        0
+
+
+
+Utilities
+=========
+
+:class:`CourseFactory`
+---------------------------
+
+.. class:: CourseFactory()
+
+   .. attribute:: grok.name(u'waeup.Course')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Course`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycourse = createObject(u'waeup.Course')
+     >>> mycourse
+     <waeup.kofa.university.course.Course object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.course import CourseFactory
+     >>> verifyClass(IFactory, CourseFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> course_factory = CourseFactory()
+     >>> course_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.course.Course>
+
+Examples
+========
+
+Creating courses
+----------------
+
+The simplest way to create a :class:`Course` instance is to import the
+class and calling the constructor:
+
+    >>> from waeup.kofa.university.course import Course
+    >>> mycourse = Course()
+    >>> mycourse
+    <waeup.kofa.university.course.Course object at 0x...>
+
+Another way to create courses is by asking for a factory called
+``waeup.Course``. This way we can create a factory without importing a
+class:
+
+    >>> from zope.component import createObject
+    >>> mycourse = createObject(u'waeup.Course')
+    >>> mycourse
+    <waeup.kofa.university.course.Course object at 0x...>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/coursescontainer.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/coursescontainer.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/coursescontainer.txt	(revision 16045)
@@ -0,0 +1,145 @@
+Course Containers
+*****************
+
+.. module:: waeup.kofa.university.coursescontainer
+
+Containers for courses.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CoursesContainer`
+-------------------------
+
+.. class:: CoursesContainer()
+
+   Create a course container instance.
+
+   .. attribute:: grok.implements(ICoursesContainer)
+
+   :class:`CoursesContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.kofa.university.course.Course`.
+
+     >>> from waeup.kofa.university.coursescontainer import CoursesContainer
+     >>> mycontainer = CoursesContainer()
+     >>> mycontainer
+     <waeup.kofa.university.coursescontainer.CoursesContainer object at 0x...>
+
+   Course containers provide `ICoursesContainer`:
+
+     >>> from waeup.kofa.university.interfaces import ICoursesContainer
+     >>> ICoursesContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CoursesContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICoursesContainer, CoursesContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CoursesContainer` instances
+   can be used like dicts:
+
+     >>> list(mycontainer.keys())
+     []
+
+   .. method:: addCourse(course)
+
+      Add a course.
+
+      *course* must be an instance of
+      :class:`waeup.kofa.university.course.Course`:
+
+        >>> from waeup.kofa.university.coursescontainer import CoursesContainer
+        >>> mycontainer = CoursesContainer()
+        >>> mycontainer.addCourse('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CoursesContainers contain only ICourse instances
+
+      The key of added items is the ``code`` attribute:
+
+        >>> from waeup.kofa.university.course import Course
+        >>> mycourse = Course(code='COURSE1')
+        >>> mycontainer.addCourse(mycourse)
+        >>> list(mycontainer.keys())
+        [u'COURSE1']
+
+   .. method:: clear()
+
+      Remove all contained courses:
+
+        >>> len(mycontainer.items())
+        1
+
+        >>> mycontainer.clear()
+        >>> len(mycontainer.items())
+        0
+
+Utilities
+=========
+
+:class:`CoursesContainerFactory`
+--------------------------------
+
+.. class:: CoursesContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CoursesContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CoursesContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycoursescontainer = createObject(u'waeup.CoursesContainer')
+     >>> mycoursescontainer
+     <waeup.kofa.university.coursescontainer.CoursesContainer object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.coursescontainer import CoursesContainerFactory
+     >>> verifyClass(IFactory, CoursesContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> coursescontainer_factory = CoursesContainerFactory()
+     >>> coursescontainer_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.coursescontainer.CoursesContainer>
+
+
+Examples
+========
+
+Getting a course container
+--------------------------
+
+We can easily create `CoursesContainers`:
+
+    >>> from waeup.kofa.university.coursescontainer import CoursesContainer
+    >>> mycontainer = CoursesContainer()
+
+
+Another way to get a course container -- without importing the class
+-- is via factories. We registered a factory for course containers
+under the name ``waeup.CoursesContainer``. Now we can ask for an object
+by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CoursesContainer')
+    <waeup.kofa.university.coursescontainer.CoursesContainer object at 0x...>
+
+This way we get a thing that implements ICoursesContainer without
+imports or similar.
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/datacenter.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/datacenter.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/datacenter.txt	(revision 16045)
@@ -0,0 +1,293 @@
+Data Center
+***********
+
+The Kofa data center cares for managing CSV files and importing then.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Creating a data center
+======================
+
+A data center can be created easily:
+
+    >>> from waeup.kofa.datacenter import DataCenter
+    >>> mydatacenter = DataCenter()
+    >>> mydatacenter
+    <waeup.kofa.datacenter.DataCenter object at 0x...>
+
+Each data center has a location in file system where files are stored:
+
+    >>> storagepath = mydatacenter.storage
+    >>> storagepath
+    '/tmp/tmp...'
+
+Beside other things it provides two locations to put data of deleted
+items into:
+
+    >>> import os
+    >>> del_path = mydatacenter.deleted_path
+    >>> os.path.isdir(del_path)
+    True
+    >>> grad_path = mydatacenter.graduated_path
+    >>> os.path.isdir(grad_path)
+    True
+
+Overall it complies with the `IDataCenter` interface:
+
+    >>> from zope.interface import verify
+    >>> from waeup.kofa.interfaces import IDataCenter
+    >>> verify.verifyObject(IDataCenter, DataCenter() )
+    True
+
+    >>> verify.verifyClass(IDataCenter, DataCenter)
+    True
+
+Managing the storage path
+=========================
+
+We can set another storage path:
+
+    >>> import os
+    >>> os.mkdir('newlocation')
+    >>> newpath = os.path.abspath('newlocation')
+    >>> mydatacenter.setStoragePath(newpath)
+    []
+
+The result here is a list of filenames, that could not be
+copied. Luckily, this list is empty.
+
+When we set a new storage path, we can tell to move all files in the
+old location to the new one. To see this feature in action, we first
+have to put a file into the old location:
+
+    >>> open(os.path.join(newpath, 'myfile.txt'), 'wb').write('hello')
+
+Now we can set a new location and the file will be copied:
+
+    >>> verynewpath = os.path.abspath('verynewlocation')
+    >>> os.mkdir(verynewpath)
+
+    >>> mydatacenter.setStoragePath(verynewpath, move=True)
+    []
+
+    >>> storagepath = mydatacenter.storage
+    >>> 'myfile.txt' in os.listdir(verynewpath)
+    True
+
+We remove the created file to have a clean testing environment for
+upcoming examples:
+
+    >>> os.unlink(os.path.join(storagepath, 'myfile.txt'))
+
+Uploading files
+===============
+
+We can get a list of files stored in that location:
+
+    >>> mydatacenter.getPendingFiles()
+    []
+
+Let's put some file in the storage:
+
+    >>> import os
+    >>> filepath = os.path.join(storagepath, 'data.csv')
+    >>> open(filepath, 'wb').write('Some Content\n')
+
+Now we can find a file:
+
+    >>> mydatacenter.getPendingFiles()
+    [<waeup.kofa.datacenter.DataCenterFile object at 0x...>]
+
+As we can see, the actual file is wrapped by a convenience wrapper,
+that enables us to fetch some data about the file. The data returned
+is formatted in strings, so that it can easily be put into output
+pages:
+
+    >>> datafile = mydatacenter.getPendingFiles()[0]
+    >>> datafile.getSize()
+    '13 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(newpath)
+    >>> shutil.rmtree(verynewpath)
+
+
+Distributing processed files
+============================
+
+When files were processed by a batch processor, we can put the
+resulting files into desired destinations.
+
+We recreate the datacenter root in case it is missing:
+
+    >>> import os
+    >>> dc_root = mydatacenter.storage
+    >>> fin_dir = os.path.join(dc_root, 'finished')
+    >>> unfin_dir = os.path.join(dc_root, 'unfinished')
+
+    >>> def recreate_dc_storage():
+    ...   if os.path.exists(dc_root):
+    ...     shutil.rmtree(dc_root)
+    ...   os.mkdir(dc_root)
+    ...   mydatacenter.setStoragePath(mydatacenter.storage)
+    >>> recreate_dc_storage()
+
+We define a function that creates a set of faked result files:
+
+    >>> import os
+    >>> import tempfile
+    >>> def create_fake_results(source_basename, create_pending=True):
+    ...   tmp_dir = tempfile.mkdtemp()
+    ...   src = os.path.join(dc_root, source_basename)
+    ...   pending_src = None
+    ...   if create_pending:
+    ...     pending_src = os.path.join(tmp_dir, 'mypendingsource.csv')
+    ...   finished_src = os.path.join(tmp_dir, 'myfinishedsource.csv')
+    ...   for path in (src, pending_src, finished_src):
+    ...     if path is not None:
+    ...       open(path, 'wb').write('blah')
+    ...   return tmp_dir, src, finished_src, pending_src
+
+Now we can create the set of result files, that typically come after a
+successful processing of a regular source:
+
+Now we can try to distribute those files. Let's start with a source
+file, that was processed successfully:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                            pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'graduated', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The created dir will be removed for us by the datacenter. This way we
+can assured, that less temporary dirs are left hanging around:
+
+    >>> os.path.exists(tmp_dir)
+    False
+
+The root dir is empty, while the original file and the file containing
+all processed data were moved to'finished/'.
+
+Now we restart, but this time we fake an erranous action:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'graduated', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+While the original source was moved to the 'unfinished' dir, the
+pending file went to the root and the set of already processed items
+are stored in finished/.
+
+We fake processing the pending file and assume that everything went
+well this time:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'graduated', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The result is the same as in the first case shown above.
+
+We restart again, but this time we fake several non-working imports in
+a row.
+
+We start with a faulty start-import:
+
+    >>> recreate_dc_storage()
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the pending file, which fails again:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+We try to process the new pending file:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv')
+    >>> mydatacenter.distProcessedFiles(False, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'graduated', 'logs', 'mysource.create.pending.csv', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    ['mysource.csv']
+
+Finally, we process the pending file and everything works:
+
+    >>> tmp_dir, src, finished_src, pending_src = create_fake_results(
+    ...  'mysource.create.pending.csv', create_pending=False)
+    >>> mydatacenter.distProcessedFiles(True, src, finished_src,
+    ...                                 pending_src, mode='create')
+
+    >>> sorted(os.listdir(dc_root))
+    ['deleted', 'finished', 'graduated', 'logs', 'unfinished']
+
+    >>> sorted(os.listdir(fin_dir))
+    ['mysource.create.finished.csv', 'mysource.csv']
+
+    >>> sorted(os.listdir(unfin_dir))
+    []
+
+The root dir is empty (contains no input files) and only the files in
+finished-subdirectory remain.
+
+
+We can get a list of imported files stored in the finished subfolder:
+
+    >>> mydatacenter.getFinishedFiles()
+    [<waeup.kofa.datacenter.DataCenterFile object at ...>]
+
+    >>> datafile = mydatacenter.getFinishedFiles()[0]
+    >>> datafile.getSize()
+    '2 bytes'
+
+    >>> datafile.getDate() # Nearly current datetime...
+    '...'
+
+
+Clean up:
+
+    >>> shutil.rmtree(verynewpath)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/department.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/department.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/department.txt	(revision 16045)
@@ -0,0 +1,135 @@
+Departments
+***********
+
+.. module:: waeup.kofa.university.department
+
+Components that represent university departments.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Department`
+-------------------
+
+.. class:: Department(title=u'Unnamed Department'[, title_prefix=u'department' [, code=u"NA"]])
+
+   Create a representation of a university department:
+
+     >>> from waeup.kofa.university.department import Department
+     >>> mydept = Department()
+     >>> mydept
+     <waeup.kofa.university.department.Department object at 0x...>
+
+   Another way to create :class:`Department` instances is by asking
+   for a factory called ``waeup.kofa.Department``. This way we can create a
+   department without importing a class:
+
+     >>> from zope.component import createObject
+     >>> mydept = createObject(u'waeup.Department')
+     >>> mydept
+     <waeup.kofa.university.department.Department object at 0x...>
+
+   :class:`Department` instances have the attributes required by the
+   `IDepartment` interface:
+
+     >>> from waeup.kofa.university.interfaces import IDepartment
+     >>> IDepartment.providedBy(mydept)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IDepartment, mydept)
+     True
+
+
+   .. attribute:: title
+
+      (string) The title of a department.
+
+      Each department has a title:
+
+        >>> mydept.title
+        u'Unnamed Department'
+
+
+   .. attribute:: title_prefix
+
+      (string) The prefix of a department.
+
+      Each department has a title prefix, which specifies the kind of
+      institution:
+
+        >>> mydept.title_prefix
+        u'department'
+
+
+   .. attribute:: code
+
+      (string) An internally used unique code string.
+
+      Each department holds a code, which might be a shortcut or
+      abbreviation of the real department name. By default the code is
+      ``NA`` (=not assigned):
+
+        >>> mydept.code
+        u'NA'
+
+
+   .. attribute:: courses
+
+      (ICoursesContainer instance) A container for courses.
+
+      Each department has a courses container:
+
+        >>> mydept.courses
+        <waeup.kofa.university.coursescontainer.CoursesContainer object at 0x...>
+
+
+   .. attribute:: certificates
+
+      (ICertificatesContainer instance) A container for certificates.
+
+      Each department has a certificate container, that holds the
+      descrtiptions of certificates the department has:
+
+        >>> mydept.certificates
+        <waeup.kofa...certificatescontainer.CertificatesContainer object at 0x...>
+
+Utilities
+=========
+
+:class:`DepartmentFactory`
+--------------------------
+
+.. class:: DepartmentFactory()
+
+   .. attribute:: grok.name(u'waeup.Department')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Department`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mydepartment = createObject(u'waeup.Department')
+     >>> mydepartment
+     <waeup.kofa.university.department.Department object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.department import DepartmentFactory
+     >>> verifyClass(IFactory, DepartmentFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> department_factory = DepartmentFactory()
+     >>> department_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.department.Department>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/facultiescontainer.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/facultiescontainer.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/facultiescontainer.txt	(revision 16045)
@@ -0,0 +1,178 @@
+Faculty Containers
+******************
+
+.. module:: waeup.kofa.university.facultiescontainer
+
+Components that represent faculty containers.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`FacultiesContainer`
+---------------------------
+
+.. class:: FacultiesContainer()
+
+   Create a faculty container:
+
+     >>> from waeup.kofa.university.facultiescontainer import FacultiesContainer
+     >>> mycontainer = FacultiesContainer()
+     >>> mycontainer
+     <waeup.kofa.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+   Another way to create :class:`FacultiesContainer` instances is by asking
+   for a factory called ``waeup.FacultiesContainer``. This way we can create a
+   faculty without importing a class:
+
+     >>> from zope.component import createObject
+     >>> mycontainer = createObject(u'waeup.FacultiesContainer')
+     >>> mycontainer
+     <waeup.kofa.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+   :class:`FacultiesContainer` instances have the attributes required by the
+   `IFacultiesContainer` interface:
+
+     >>> from waeup.kofa.university.interfaces import IFacultiesContainer
+     >>> IFacultiesContainer.providedBy(mycontainer)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IFacultiesContainer, mycontainer)
+     True
+
+   .. method:: addFaculty(faculty)
+
+      Add a faculty into this container. Added objects are checked and
+      only `IFaculty` objects accepted:
+
+        >>> mycontainer.addFaculty(object())
+        Traceback (most recent call last):
+        ...
+        TypeError: FacultiesContainers contain only IFaculty instances
+
+        >>> list(mycontainer.values())
+        []
+
+      Regular faculties are accepted:
+
+        >>> from waeup.kofa.university.faculty import Faculty
+        >>> mycontainer.addFaculty(Faculty(title='Physics',
+        ...                                code='FP'))
+
+        >>> list(mycontainer.items())
+        [(u'FP', <waeup.kofa.university.faculty.Faculty object at 0x...>)]
+
+
+
+Utilities
+=========
+
+:class:`AcademicsPlugin`
+------------------------
+
+.. class:: AcademicsPlugin()
+
+   .. attribute:: grok.implements(IKofaPluggable)
+
+   This plugin component tells under which name (``faculties``) an
+   instance of academics stuff should be created in plain Kofa
+   instances. It also tells the factory name for FacultiesContainer
+   instances, which serve as root object for academics stuff in Kofa apps.
+
+
+:class:`FacultiesContainerFactory`
+----------------------------------
+
+.. class:: FacultiesContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.FacultiesContainer')
+
+   .. attribute:: grok.implements(IFactoryContainer)
+
+   A named utility to deliver new instances of :class:`FacultiesContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycontainer = createObject(u'waeup.FacultiesContainer')
+     >>> mycontainer
+     <waeup.kofa.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.facultiescontainer import (
+     ...   FacultiesContainerFactory)
+     >>> verifyClass(IFactory, FacultiesContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> fac_container_factory = FacultiesContainerFactory()
+     >>> fac_container_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.facultiescontainer.FacultiesContainer>
+
+Examples
+========
+
+We can easily create `FacultiesContainers`:
+
+    >>> from waeup.kofa.university.facultiescontainer import FacultiesContainer
+    >>> mycontainer = FacultiesContainer()
+
+Faculty containers provide `IFacultiesContainer`:
+
+    >>> from waeup.kofa.university.interfaces import IFacultiesContainer
+    >>> IFacultiesContainer.providedBy(mycontainer)
+    True
+
+Another way to get a faculty container -- without importing the class
+-- is via factories. We registered a factory for faculty containers
+under the name ``waeup.facultiescontainer``:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.FacultiesContainer')
+    <waeup.kofa.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+This way we get a thing that implements IFacultiesContainer without
+imports or similar.
+
+We can be sure, that the full interface is supported by the
+FacultiesContainer class::
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IFacultiesContainer, FacultiesContainer)
+    True
+
+
+Storing things in faculty containers
+====================================
+
+We can, of course, store things in a faculty container. But when we
+really store an object, then it must be a faculty::
+
+    >>> mycontainer.addFaculty(42)
+    Traceback (most recent call last):
+    ...
+    TypeError: FacultiesContainers contain only IFaculty instances
+
+Okay, so we have to get a faculty first::
+
+    >>> from waeup.kofa.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+
+We can add this faculty to our container::
+
+    >>> mycontainer.addFaculty(myfaculty)
+
+We get back the key, under which the faculty was stored. It will be
+some string, but there is no guarantee at all, how this key looks
+like. It might be a string of integers, a name or whatever; you cannot
+know before.
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/faculty.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/faculty.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/faculty.txt	(revision 16045)
@@ -0,0 +1,200 @@
+Faculties
+*********
+
+.. module:: waeup.kofa.university.faculty
+
+Components that represent university faculties.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+
+:class:`Faculty`
+----------------
+
+.. class:: Faculty(title=u'Unnamed Faculty'[, title_prefix=u'faculty' [, code=u"NA"]])
+
+   Create a representation of a university faculty:
+
+     >>> from waeup.kofa.university.faculty import Faculty
+     >>> myfac = Faculty()
+     >>> myfac
+     <waeup.kofa.university.faculty.Faculty object at 0x...>
+
+   Another way to create :class:`Faculty` instances is by asking
+   for a factory called ``waeup.Faculty``. This way we can create a
+   faculty without importing a class:
+
+     >>> from zope.component import createObject
+     >>> myfac = createObject(u'waeup.Faculty')
+     >>> myfac
+     <waeup.kofa.university.faculty.Faculty object at 0x...>
+
+   :class:`Faculty` instances have the attributes required by the
+   `IFaculty` interface:
+
+     >>> from waeup.kofa.university.interfaces import IFaculty
+     >>> IFaculty.providedBy(myfac)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IFaculty, myfac)
+     True
+
+   .. method:: addDepartment(department)
+
+      Add a department into this faculty. Faculties are containers for
+      departments. Added objects are checked and only `IDepartment`
+      objects accepted:
+
+        >>> myfac.addDepartment(object())
+        Traceback (most recent call last):
+        ...
+        TypeError: Faculties contain only IDepartment instances
+
+        >>> list(myfac.values())
+        []
+
+      Regular departments are accepted:
+
+        >>> from waeup.kofa.university.department import Department
+        >>> myfac.addDepartment(Department(title='Physics',
+        ...                                code='DP'))
+
+        >>> list(myfac.items())
+        [(u'DP', <waeup.kofa.university.department.Department object at 0x...>)]
+
+
+   .. attribute:: title
+
+      (string) The title of a faculty.
+
+      Each faculty has a title:
+
+        >>> myfac.title
+        u'Unnamed Faculty'
+
+
+   .. attribute:: title_prefix
+
+      (string) The prefix of a faculty.
+
+      Each faculty has a title prefix, which specifies the kind of
+      institution:
+
+        >>> myfac.title_prefix
+        u'faculty'
+
+
+   .. attribute:: code
+
+      (string) An internally used unique code string.
+
+      Each faculty holds a code, which might be a shortcut or
+      abbreviation of the real faculty name. By default the code is
+      ``NA`` (=not assigned):
+
+        >>> myfac.code
+        u'NA'
+
+
+Utilities
+=========
+
+:class:`FacultyFactory`
+--------------------------
+
+.. class:: FacultyFactory()
+
+   .. attribute:: grok.name(u'waeup.Faculty')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`Faculty`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> myfaculty = createObject(u'waeup.Faculty')
+     >>> myfaculty
+     <waeup.kofa.university.faculty.Faculty object at 0x...>
+
+   The factory complies with the specifications from the
+   :class:`IFactory` insterface:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> from zope.component.interfaces import IFactory
+     >>> from waeup.kofa.university.faculty import FacultyFactory
+     >>> verifyClass(IFactory, FacultyFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> faculty_factory = FacultyFactory()
+     >>> faculty_factory.getInterfaces()
+     <implementedBy waeup.kofa.university.faculty.Faculty>
+
+
+Examples:
+=========
+
+We can create faculties:
+
+    >>> from waeup.kofa.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+    >>> myfaculty
+    <waeup.kofa.university.faculty.Faculty object at 0x...>
+
+Another way to create faculties is by asking for a factory called
+``waeup.Faculty``. Now we can create a factory without importing a
+class:
+
+    >>> from zope.component import createObject
+    >>> myfaculty = createObject(u'waeup.Faculty')
+    >>> myfaculty
+    <waeup.kofa.university.faculty.Faculty object at 0x...>
+
+Faculty attributes
+==================
+
+Faculties have the attributes required by the `IFaculty` interface:
+
+    >>> from waeup.kofa.university.interfaces import IFaculty
+    >>> IFaculty.providedBy(myfaculty)
+    True
+
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(IFaculty, myfaculty)
+    True
+
+Each of the following attributes is mandatory.
+
+`title`
+-------
+
+Each faculty has a title:
+
+    >>> myfaculty.title
+    u'Unnamed Faculty'
+
+`title_prefix`
+--------------
+
+Each faculty has a title prefix, which specifies the kind of
+institution:
+
+    >>> myfaculty.title_prefix
+    u'faculty'
+
+`code`
+------
+
+Each faculty holds a code, which might be a shortcut or abbreviation
+of the real faculty name. By default the code is ``NA`` (=not assigned):
+
+    >>> myfaculty.code
+    u'NA'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/helpers.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/helpers.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/helpers.txt	(revision 16045)
@@ -0,0 +1,228 @@
+Helpers for Kofa
+****************
+
+.. module:: waeup.kofa.utils.helpers
+
+Helper functions for Kofa.
+
+.. :doctest:
+
+:func:`remove_file_or_directory`
+================================
+
+.. function:: remove_file_or_directory(path)
+
+   Removes a file or directory given by a path. We can remove files:
+
+     >>> import os
+     >>> import tempfile
+     >>> old_location = os.getcwd()
+     >>> new_location = tempfile.mkdtemp()
+     >>> os.chdir(new_location)
+
+     >>> from waeup.kofa.utils.helpers import remove_file_or_directory
+     >>> open('blah', 'wb').write('nonsense')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> remove_file_or_directory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+   We can remove directories:
+
+     >>> os.mkdir('blah')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> remove_file_or_directory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+
+:func:`copy_filesystem_tree`
+============================
+
+.. function:: ccopy_filesystem_tree(src_path, dst_path[, overwrite=False[, del_old=False]])
+
+   Copies the contents of an (existing) directory to another
+   (existing) directory.
+
+   :param src_path: filesystem path to copy from
+   :type  src_path: string
+   :param dst_path: filesystem path to copy to
+   :type  dst_path: string
+   :keyword overwrite: Whether exiting files with same names should be
+                     overwritten.
+   :type  overwrite: bool
+   :keyword del_old: Whether old contents in destination path should be 
+                   removed.
+   :type  del_old: bool
+   :return: List of non-copied files
+ 
+   Both directories must exist.
+
+   Unix hidden files and directories (starting with '.') are not
+   processed by this function.
+
+   Without any further parameters, we can copy complete file trees:
+
+     >>> os.mkdir('src')
+     >>> os.mkdir('dst')
+     >>> open(os.path.join('src', 'blah'), 'wb').write('nonsense')
+
+     >>> from waeup.kofa.utils.helpers import copy_filesystem_tree
+     >>> result = copy_filesystem_tree('src', 'dst')
+
+   As a result we get a list of non-copied files:
+
+     >>> result
+     []
+
+   The created file was indeed copied:
+
+     >>> 'blah' in os.listdir('dst')
+     True
+
+   Hidden files (i.e. such starting with a dot) are not copied:
+
+     >>> open(os.path.join('src', '.blah'), 'wb').write('nonsense')
+     >>> result = copy_filesystem_tree('src', 'dst')
+     >>> '.blah' in os.listdir('dst')
+     False
+
+   This function supports some keyword parameters as explained below.
+
+Using ``overwrite``
+-------------------
+
+Boolean. If set to ``True``, any existing and same named files and
+directories in the destination dir are overwritten with copies from
+the source. Default is `False`.
+
+Normally, existing same named files in the destination are not
+overwritten:
+
+    >>> open(os.path.join('src', 'blah'), 'wb').write('newnonsense')
+    >>> result = copy_filesystem_tree('src', 'dst')
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'nonsense'
+
+Instead the filename is added to the result (a list of non-copied
+files):
+
+    >>> result
+    ['blah']
+
+If, however, we use `overwrite`, the existing file will be
+overwritten:
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'blah'), 'rb').read()
+    'newnonsense'
+
+    >>> result
+    []
+
+This also works for complete directories:
+
+    >>> os.mkdir(os.path.join('src', 'mydir'))
+    >>> os.mkdir(os.path.join('dst', 'mydir'))
+    >>> open(os.path.join(
+    ...   'src', 'mydir', 'blah'), 'wb').write('srcblah')
+    >>> open(os.path.join(
+    ...   'dst', 'mydir', 'blah'), 'wb').write('dstblah')
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True)
+    >>> open(os.path.join('dst', 'mydir', 'blah'), 'rb').read()
+    'srcblah'
+
+
+Using ``del_old``
+-----------------
+
+Boolean. If set to ``True``, any copied files and directories will be
+removed from the src dir. Default is `False`.
+
+    >>> result = copy_filesystem_tree('src', 'dst', overwrite=True,
+    ...                                           del_old=True)
+    >>> os.listdir('src')
+    ['.blah']
+
+All files and directories are removed from src, except the hidden file
+we created in the beginning.
+
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(new_location)
+    >>> os.chdir(old_location)
+    
+
+
+:func:`get_inner_HTML_part()`
+=============================
+
+.. function:: get_inner_HTML_part(html_code)
+
+   Get the 'inner' part out of a piece of HTML code.
+
+   Helper function mainly to extract 'real content' from already
+   rendered forms.
+
+   The term 'inner part' here means the ``<form>`` part of an HTML
+   snippet. If this cannot be found, we look for a ``<body>`` part and
+   if this cannot be found as well, we simply return the whole input.
+
+   If a ``<form>`` part can be found in an HTML snippet, this is
+   returned with all preceeding/following stuff stripped:
+
+     >>> from waeup.kofa.utils.helpers import get_inner_HTML_part
+     >>> print get_inner_HTML_part("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ... <form action="http://localhost/myuniversity/faculties/TF/add"
+     ...       method="post" class="edit-form"
+     ...       enctype="multipart/form-data">
+     ...   <h1>Add a department</h1>
+     ... </form>
+     ... </body>
+     ... </html>
+     ... """)
+     <BLANKLINE>
+     <form action="http://localhost/myuniversity/faculties/TF/add"
+           method="post" class="edit-form"
+           enctype="multipart/form-data">
+     <BLANKLINE>
+       <h1>Add a department</h1>
+     </form>
+     <BLANKLINE>
+     <BLANKLINE>
+
+   If there is no ``<form>`` part, try to find any ``<body>`` part:
+
+     >>> print get_inner_HTML_part("""<html>
+     ... <head>
+     ... </head>
+     ... <BLANKLINE>
+     ... <body>
+     ...  <div>Some content</div>
+     ... </body>
+     ... </html>
+     ... """)
+     <BLANKLINE>
+      <div>Some content</div>
+     <BLANKLINE>
+
+   If there is also no ``<body>`` tag, we return the input as-is:
+
+     >>> print get_inner_HTML_part("""<div>
+     ...  <div>Some content</div>
+     ... </div>
+     ... """)
+     <div>
+      <div>Some content</div>
+     </div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/pages.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/pages.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/pages.txt	(revision 16045)
@@ -0,0 +1,1343 @@
+Browsing Kofa
+*************
+
+Here we visit parts of a Kofa portal using a browser.
+
+University
+==========
+
+We can watch universities in the browser.
+
+We create an university object and put into the ZODB root::
+
+  >>> root = getRootFolder()
+  >>> list(root)
+  []
+
+  >>> from waeup.kofa.app import University
+  >>> u = University()
+  >>> root['myuniversity'] = u
+  >>> list(root)
+  [u'myuniversity']
+
+  >>> from zope.component.hooks import setSite
+  >>> setSite(root['myuniversity'])
+
+To make sure, we can 'watch' pages, we first have to initialize out
+test browser::
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+
+Let's get the default view of a university::
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Welcome to WAeUP.Kofa...
+  ...
+
+We can change to German::
+
+  >>> browser.getLink('de').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Anfragen...
+  ...
+
+And then change back to English by clicking on the third link containing 'en'
+behind 'Anfragen' and 'Einloggen'::
+
+  >>> browser.getLink('en', index=2).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Enquiries...
+  ...
+
+The contact form for anonymous users is called 'Enquiries'::
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Enquiries').click()
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Required input is missing...
+  ...
+
+  >>> browser.getControl(name='form.fullname').value = "Bob Tester"
+  >>> browser.getControl(name='form.email_from').value = "xx@yy.zz"
+  >>> browser.getControl(name='form.body').value = u'test message'
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+Registered users with an email address can request a password change:
+
+  >>> root['myuniversity']['users'].addUser('forgetful', 'secret',title='Bob Forgetful',
+  ...                               description='A forgetful user', email='aa@aa.ng')
+  >>> browser.open('http://localhost/myuniversity/changepw')
+  >>> browser.getControl(name="form.identifier").value = 'forgetful'
+  >>> browser.getControl(name="form.email").value = 'aa@aa.ng'
+  >>> browser.getControl("Send login credentials").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...An email with your user name and password has been sent to aa@aa.ng...
+
+Now we login as manager::
+
+  >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+  >>> browser.handleErrors = False
+
+We can then get an edit view of the configuration container::
+
+  >>> browser.open('http://localhost/myuniversity/configuration')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<form action="http://localhost/myuniversity/configuration/@@index"
+  ...
+
+The edit form contains the default value for the university name::
+
+  >>> 'Sample University' in browser.contents
+  True
+
+We can perform several actions on the edit form::
+
+  >>> browser.getControl("Save", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Form has been saved...
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/configuration')
+  >>> browser.getControl("Update plugins").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Plugins were updated. See log file for details...
+  ...
+
+The default frontpage HTML has been saved in a dictionary
+and is properly rendered on the frontpage of the portal:
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Welcome to WAeUP.Kofa...</h1>
+  ...
+
+The German part is really not being rendered:
+
+  >>> 'Willkommen' in browser.contents
+  False
+
+
+If we change to German so that the German part of frontpage.rst is rendered:
+
+  >>> browser.open('http://localhost/myuniversity//@@change_language?lang=de')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1>Willkommen auf WAeUP.Kofa...</h1>
+  ...
+
+The English part is really not being rendered:
+
+  >>> 'Welcome' in browser.contents
+  False
+
+Switch back to English:
+
+  >>> browser.open('http://localhost/myuniversity//@@change_language?lang=en')
+
+
+Officers
+========
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Officers').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Officers...
+  ...
+
+We can add officers:
+
+  >>> browser.getLink("Add officer").click()
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl("Add officer").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Passwords must be at least 8 characters long...
+  >>> browser.getControl(name="password").value = 'TOP4secret'
+  >>> browser.getControl(name="control_password").value = 'TOP4secret'
+  >>> browser.getControl("Add officer").click()
+
+We have been redirected to the Send Password page:
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Send user a new password...
+  ...
+
+We can edit user bob:
+
+  >>> browser.open('http://localhost/myuniversity/users')
+  >>> browser.getControl("Manage", index=0).click()
+  >>> browser.getControl("Save", index=0).click()
+  >>> browser.getControl("Cancel", index=0).click()
+
+We can add site roles which are then displayed on the user container page.
+Since the test browser does not use javascript, we have to add site roles
+manually by setting the roles attribute:
+
+  >>> print root['myuniversity']['users']['bob'].roles
+  []
+  >>> root['myuniversity']['users']['bob'].roles = ['waeup.ApplicationsOfficer']
+  >>> print root['myuniversity']['users']['bob'].roles
+  ['waeup.ApplicationsOfficer']
+  >>> browser.open('http://localhost/myuniversity/users')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td nowrap>Applications Officer (view only) <br /></td>
+  ...
+
+Officers can't be added twice:
+
+  >>> browser.open('http://localhost/myuniversity/users/add')
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'TOP4secret'
+  >>> browser.getControl(name="control_password").value = 'TOP4secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone.country").value = ['+234']
+  >>> browser.getControl(name="form.phone.area").value = '123'
+  >>> browser.getControl(name="form.phone.ext").value = '45678'
+  >>> browser.getControl("Add officer").click()
+  >>> 'The userid chosen already exists' in browser.contents
+  True
+
+Officers can be deleted:
+
+  >>> browser.open('http://localhost/myuniversity/users')
+  >>> browser.getControl("Remove", index=0).click()
+  >>> 'User account bob successfully deleted' in browser.contents
+  True
+
+
+
+Contact Form
+============
+
+Let's enter the contact form::
+
+  >>> browser.open('http://localhost/myuniversity/contactadmin')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Contact
+  ...
+
+We fill the form  (this will send a real message to
+contact@waeup.org)::
+
+  >>> browser.open('http://localhost/myuniversity/contactadmin')
+  >>> browser.getControl(name='form.body').value = "test message"
+  >>> browser.getControl('Send').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Your message has been sent...
+  ...
+
+If this test fails, chances are, that the local machine has no SMTP
+server installed.
+
+
+
+Faculties
+=========
+
+Faculties are stored in a special container of `IUniversity`
+instances. The container is called ``faculties`` and provides an
+add-form to add new faculties::
+
+  >>> browser.open('http://localhost/myuniversity/faculties/manage')
+  >>> browser.getControl('Add faculty').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...*...
+  ...<span>Name of faculty</span>:
+  ...
+  >>> browser.getControl('Cancel').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/add')
+
+We fill in a new name for our new faculty::
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'TestFac'
+
+Furthermore we add a prefix and a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'TF'
+
+Finally we click on 'Add Faculty' to add the new thing::
+
+  >>> browser.getControl('Add faculty').click()
+
+We can view a faculty by browsing a URL like this::
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+
+Afterwards, the faculty should be visible:
+
+  >>> browser.open('http://localhost/myuniversity/faculties')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...<h1 class="kofa-content-label">Academic Section</h1>
+  ...<td> <a href="http://localhost/myuniversity/faculties/TF"> <span>TF</span></a></td>
+  ...
+
+We can 'visit' each faculty by clicking on the appropriate link:
+
+  >>> browser.getLink('TF').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Faculty of TestFac (TF)...
+  ...
+
+If we add the same faculty twice, an error message will occur:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/add')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'TestFac'
+  >>> browser.getControl(name='form.code').value = 'TF'
+  >>> browser.getControl('Add faculty').click()
+  >>> 'The faculty code chosen already exists.' in browser.contents
+  True
+
+Modifying faculties
+-------------------
+
+A faculty can directly be reached by its code:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+
+We can change the settings for a faculty by clicking on the
+provided 'Manage  faculty' button:
+
+  >>> browser.getLink('Manage faculty').click()
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My renamed faculty"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Our faculty was indeed renamed to ``My renamed faculty``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1 class="kofa-content-label">Academic Section</h1>
+  ...<td> <a href="http://localhost/myuniversity/faculties/TF"> <span>TF</span></a></td>
+  ...<td>Faculty of My renamed faculty (TF)</td>
+  ...
+
+We can grant local roles:
+
+  >>> root['myuniversity']['users'].addUser('bob', 'secret',title='Bob',
+  ...                               description='A sample user')
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> browser.getControl(name="user").value = ['bob']
+  >>> browser.getControl(
+  ...     name="local_role").value = ['waeup.local.DepartmentManager']
+  >>> browser.getControl("Add local role").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+On the officers page the new local role is displayed:
+
+  >>> browser.getLink("Officers").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>Department Manager:...
+  ...
+
+The local role can be removed again:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...No local role selected...
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> ctrl.getControl(value='bob|waeup.local.DepartmentManager').selected = True
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Local role successfully removed...
+  ...
+
+Deleting faculties
+------------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/manage')
+  >>> browser.getControl('Cancel').click()
+  >>> print browser.url
+  http://localhost/myuniversity/faculties
+  >>> browser.getLink('Manage academic section').click()
+  >>> browser.getControl('Add faculty').click()
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Second Faculty'
+  >>> browser.getControl(name='form.code').value = 'TF2'
+  >>> browser.getControl('Add faculty').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/manage')
+  >>> browser.getControl("Remove selected", index=0).click()
+  >>> 'No item selected' in browser.contents
+  True
+  >>> browser.open('http://localhost/myuniversity/faculties/manage')
+  >>> ctrl = browser.getControl(name='val_id')
+  >>> ctrl.getControl(value='TF2').selected = True
+  >>> browser.getControl("Remove selected", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully removed:...
+  ...
+
+Departments
+===========
+
+Adding departments
+------------------
+
+Departments are stored in :class:`IFaculty` instances with their code
+as key. Faculties therefore are also department containers. Faculties
+provides an add-form to add new departments:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/add')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...*</span>
+  ...<span>Name of department</span>:
+  ...
+  >>> browser.getControl('Cancel').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/add')
+
+We fill in a new name for our new department:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'TestDept'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'TD'
+
+Finally we click on 'Add Department' to add the new thing::
+
+  >>> browser.getControl('Add department').click()
+
+If we try to register a department under the same code twice we will
+get an error:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/add')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another TestDept with same code'
+  >>> browser.getControl(name='form.code').value = 'TD'
+  >>> browser.getControl('Add department').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...The code chosen already exists in this faculty...
+  ...
+
+We can view a department by browsing a URL like this::
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+
+Afterwards, the department should be visible::
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<h1 class="kofa-content-label">Departments</h1>
+  ...<td> <a href="http://localhost/myuniversity/faculties/TF/TD"> <span>TD</span></a></td>
+  ...<td>Department of TestDept (TD)</td>
+  ...
+
+
+Modifying departments
+---------------------
+
+We can change the settings for a department by clicking on the
+provided 'Edit department' button:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> browser.getLink('Manage department').click()
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test dept"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the department again.
+
+  >>> browser.getControl(name='form.title').value = "My renamed dept"
+  >>> ctrl = browser.getControl("Save")
+  >>> ctrl.click()
+
+
+Our department was indeed renamed to ``My renamed dept``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...<h1 class="kofa-content-label">Departments</h1>
+  ...<td> <a href="http://localhost/myuniversity/faculties/TF/TD"> <span>TD</span></a></td>
+  ...<td>Department of My renamed dept (TD)</td>
+  ...
+
+We can grant local roles:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/manage')
+  >>> browser.getControl(name="user").value = ['bob']
+  >>> browser.getControl(
+  ...     name="local_role").value = ['waeup.local.DepartmentManager']
+  >>> browser.getControl("Add local role").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+The local role can be removed again:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...No local role selected...
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> ctrl.getControl(
+  ...     value='bob|waeup.local.DepartmentManager').selected = True
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Local role successfully removed...
+  ...
+
+Deleting departments
+--------------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> browser.getControl('Cancel', index=0).click()
+  >>> print browser.url
+  http://localhost/myuniversity/faculties/TF
+  >>> browser.getLink('Manage faculty').click()
+  >>> browser.getControl('Add department').click()
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Second Department'
+  >>> browser.getControl(name='form.code').value = 'TD2'
+  >>> browser.getControl('Add department').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> browser.getControl("Remove selected", index=0).click()
+  >>> 'No item selected' in browser.contents
+  True
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> ctrl = browser.getControl(name='val_id')
+  >>> ctrl.getControl(value='TD2').selected = True
+  >>> browser.getControl("Remove selected", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully removed:...
+  ...
+
+
+Courses
+=======
+
+Once we have a department, we can add courses.
+
+Adding courses
+--------------
+
+Courses are stored in :class:`ICoursesContainer` instances with their
+code as key. CoursesContainers are normally availabe as `course`
+attribute of :class:`waeup.kofa.university.department.Department`
+instances.
+
+To ease the life of officers we do not require to browse the
+coursescontainers (which have a rather flat user interface), but
+provide adding of courses in department views.
+
+Each department provides a ``Add course`` action button near top.
+
+Departments provide an add-form to add new courses:
+
+  >>> dept_url = 'http://localhost/myuniversity/faculties/TF/TD'
+  >>> browser.open(dept_url + '/manage')
+  >>> browser.getControl('Add course').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...*</span>
+  ...<span>Title of course</span>:
+  ...
+
+We fill in a name for our new course:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'My Course 1'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+
+This course will take place in the the first semester, so we set the
+`semester` value to 1:
+
+  >>> ctrl = browser.getControl(name='form.semester')
+  >>> ctrl.options
+  ['1', '2', '3', '9']
+
+  >>> ctrl.displayOptions
+  ['1st Semester', '2nd Semester', 'Combined', 'N/A']
+
+  >>> ctrl.value = ['1']
+
+Finally, we create the course:
+
+  >>> browser.getControl('Add course').click()
+
+If we try to register a course under the same code twice we will
+get an error:
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+  >>> browser.getControl('Add course').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...A course with same code already exists:...
+  ...
+
+Our course will be linked under the code on the department page:
+
+  >>> browser.open(dept_url)
+  >>> browser.getLink('COURSE1').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/courses/COURSE1'
+
+Before we really add a course we can cancel the action and will be
+redirected to the department page:
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD'
+
+
+Modifying courses
+-----------------
+
+We can change the settings for a course by clicking on the provided
+'Edit settings' link:
+
+  >>> browser.open(dept_url + '/courses/COURSE1')
+  >>> browser.getLink('Edit course').click()
+
+When modifying a course, we cannot change the code any more:
+
+  >>> browser.getControl(name='form.code')
+  Traceback (most recent call last):
+  ...
+  LookupError: name 'form.code'
+  available items:
+    <TextControl(form.title=My Course 1)>
+    ...
+
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test course"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the course again. If we click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our course was not renamed to ``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>My test course</td>...
+  ...
+
+Searching courses
+-----------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "My test course"
+  >>> browser.getControl('Search').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...faculties/TF/TD/courses/COURSE1">COURSE1</a>
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "COURSE1"
+  >>> browser.getControl('Search').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...faculties/TF/TD/courses/COURSE1">COURSE1</a>
+  ...
+
+  >>> browser.getControl(name='query').value = "Nonsense"
+  >>> browser.getControl('Search').click()
+  >>> 'Search Results' in browser.contents
+  False
+  >>> '<div class="alert alert-warning">No object found.</div>' in browser.contents
+  True
+
+  >>> browser.getControl(name='query').value = ""
+  >>> browser.getControl('Search').click()
+  >>> 'Empty search string' in browser.contents
+  True
+
+We can grant local roles:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/courses/COURSE1/manage')
+  >>> browser.getControl(name="user").value = ['bob']
+  >>> browser.getControl(
+  ...     name="local_role").value = ['waeup.local.Lecturer']
+  >>> browser.getControl("Add local role").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+On the portal user page the new local role is displayed:
+
+  >>> browser.getLink("Officers").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>Lecturer:...
+  ...
+
+The local role can be removed again:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/courses/COURSE1/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...No local role selected...
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD/courses/COURSE1/manage')
+  >>> ctrl = browser.getControl(name='role_id')
+  >>> ctrl.getControl(value='bob|waeup.local.Lecturer').selected = True
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Local role successfully removed...
+  ...
+
+Deleting courses
+----------------
+
+We can delete courses by browsing the manage page of the containing
+department and checking the appropriate select box and clicking the
+´´Remove selected´´ button.
+
+  >>> browser.open(
+  ...     'http://localhost/myuniversity/faculties/TF/TD/@@manage#tab-2')
+  >>> 'My test course' in browser.contents
+  True
+
+  >>> browser.getControl('Cancel', index=0).click()
+  >>> browser.getLink('Manage department').click()
+  >>> browser.getControl('Remove selected courses').click()
+  >>> 'No item selected' in browser.contents
+  True
+
+  >>> browser.getControl(
+  ...    name='val_id').getControl(value='COURSE1').selected = True
+  >>> browser.getControl('Remove selected courses').click()
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> 'My renamed course' in browser.contents
+  False
+
+
+Deleting departments with courses
+---------------------------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> browser.getControl('Cancel', index=0).click()
+  >>> browser.getLink('Manage faculty').click()
+  >>> browser.getControl('Add department').click()
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Third Department'
+  >>> browser.getControl(name='form.code').value = 'TD3'
+  >>> browser.getControl('Add department').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD3/manage')
+  >>> browser.getControl('Add course').click()
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'My Course 5'
+  >>> browser.getControl(name='form.code').value = 'COURSE5'
+  >>> browser.getControl('Add course').click()
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/manage')
+  >>> ctrl = browser.getControl(name='val_id')
+  >>> ctrl.getControl(value='TD3').selected = True
+  >>> browser.getControl("Remove selected", index=0).click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Successfully removed:...
+  ...
+
+Certificates
+============
+
+Once we have a department, we can add also certificates.
+
+Adding certificates
+-------------------
+
+Certificates are stored in :class:`ICertificatesContainer` instances
+with their code as key. CertificatesContainers are normally availabe as
+`certificates` attribute of
+:class:`waeup.kofa.university.department.Department` instances.
+
+To ease the life of officers we do not require to browse the
+certificatescontainers (which have in fact no user interface), but
+provide adding of certificates in department views.
+
+Each department provides a ``Add certificate`` action button near top.
+
+Departments provide an add-form to add new certificates:
+
+  >>> dept_url = 'http://localhost/myuniversity/faculties/TF/TD'
+  >>> browser.open(dept_url + '/manage')
+  >>> browser.getControl('Add certificate').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...*</span>
+  ...<span>Title</span>:
+  ...
+
+We fill in a name for our new cert:
+
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'My Certificate 1'
+
+Furthermore we add a code (kind of abbreviation):
+
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+
+Set the remaining required fields:
+
+  >>> browser.getControl(name='form.study_mode').value = ['ug_ft']
+  >>> browser.getControl(name='form.start_level').value = ['100']
+  >>> browser.getControl(name='form.end_level').value = ['400']
+  >>> browser.getControl(name='form.application_category').value = ['basic']
+
+Finally, we create the certificate:
+
+  >>> browser.getControl('Add certificate').click()
+
+If we try to register a certificate under the same code twice we will
+get an error:
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+  >>> browser.getControl(name='form.study_mode').value = ['ug_ft']
+  >>> browser.getControl(name='form.start_level').value = ['100']
+  >>> browser.getControl(name='form.end_level').value = ['400']
+  >>> browser.getControl(name='form.application_category').value = ['basic']
+
+  >>> browser.getControl('Add certificate').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...A certificate with same code already exists:...
+  ...
+
+Our certificate will be linked under the code on the department page:
+
+  >>> browser.open(dept_url)
+  >>> browser.getLink('CERT1').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/certificates/CERT1'
+
+Before we really add a certificate we can cancel the action and will be
+redirected to the department page:
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD'
+
+
+Modifying certificates
+----------------------
+
+We can change the settings for a cert by clicking on the provided
+'Edit certificate' link:
+
+  >>> browser.open(dept_url + '/certificates/CERT1')
+  >>> browser.getLink('Manage certificate').click()
+
+When modifying a certificate, we cannot change the code any more:
+
+  >>> browser.getControl(name='form.code')
+  Traceback (most recent call last):
+  ...
+  LookupError: name 'form.code'
+  available items:
+    <TextControl(form.title=My Certificate 1)>
+    ...
+
+
+Let's set a new title and save the form:
+
+  >>> browser.getControl(name='form.title').value = "My test cert"
+  >>> browser.getControl(name='form.actions.save').click()
+
+Clicking 'Save' we will stay on the settings form. So we can change
+the cert again.
+
+  >>> browser.getControl(name='form.title').value = "My renamed cert"
+  >>> ctrl = browser.getControl("Save",index=0)
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel',index=0).click()
+
+Our certificate was indeed renamed to ``My renamed cert`` and not to
+``Blah``:
+
+  >>> browser.open('http://localhost/myuniversity/faculties/TF/TD')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>My renamed cert</td>...
+  ...
+
+Searching certificates
+----------------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "My renamed cert"
+  >>> browser.getControl('Search').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...faculties/TF/TD/certificates/CERT1">CERT1</a>
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "CERT1"
+  >>> browser.getControl('Search').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...faculties/TF/TD/certificates/CERT1">CERT1</a>
+  ...
+
+Deleting certificates
+---------------------
+
+We can delete certificates by browsing the manage page of the
+containing department and checking the appropriate select box and
+clicking the ´´Remove selected´´ button.
+
+  >>> browser.open(
+  ...    'http://localhost/myuniversity/faculties/TF/TD/@@manage#tab-3')
+  >>> 'My renamed cert' in browser.contents
+  True
+
+  >>> browser.getControl('Remove selected certificates').click()
+  >>> 'No item selected' in browser.contents
+  True
+
+  >>> browser.getControl(name='val_id').getControl(
+  ...    value='CERT1').selected = True
+  >>> browser.getControl('Remove selected certificates').click()
+  >>> 'My renamed cert' in browser.contents
+  False
+
+
+Certificate Courses
+===================
+
+Once we have a certificate, we can add also certificate courses. These
+are referrers of courses with some extra data.
+
+Before we can work with certificate courses, we need some certificates
+and courses to be available.
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE1'
+  >>> browser.getControl(name='form.title').value = 'Course 1'
+  >>> browser.getControl('Add course').click()
+
+  >>> browser.open(dept_url + '/addcourse')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another course with same code'
+  >>> browser.getControl(name='form.code').value = 'COURSE2'
+  >>> browser.getControl(name='form.title').value = 'Course 2'
+  >>> browser.getControl('Add course').click()
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT1'
+  >>> browser.getControl(name='form.title').value = 'Certificate 1'
+  >>> browser.getControl(name='form.study_mode').value = ['ug_ft']
+  >>> browser.getControl(name='form.start_level').value = ['100']
+  >>> browser.getControl(name='form.end_level').value = ['400']
+  >>> browser.getControl(name='form.application_category').value = ['basic']
+  >>> browser.getControl('Add certificate').click()
+
+  >>> browser.open(dept_url + '/addcertificate')
+  >>> ctrl = browser.getControl(name='form.title')
+  >>> ctrl.value = 'Another cert with same code'
+  >>> browser.getControl(name='form.code').value = 'CERT2'
+  >>> browser.getControl(name='form.title').value = 'Certificate 2'
+  >>> browser.getControl(name='form.study_mode').value = ['ug_ft']
+  >>> browser.getControl(name='form.start_level').value = ['100']
+  >>> browser.getControl(name='form.end_level').value = ['400']
+  >>> browser.getControl(name='form.application_category').value = ['basic']
+  >>> browser.getControl('Add certificate').click()
+
+
+Adding certificate courses
+--------------------------
+
+Certcourses are stored in :class:`ICertificate` instances
+with their code as key.
+
+Each certificate provides a ``Add certificate course`` action button near top.
+
+Certificates provide an add-form to add new certcourses:
+
+  >>> cert_url = dept_url + '/certificates/CERT1'
+  >>> browser.open(cert_url + '/manage')
+  >>> browser.getControl('Add certificate course').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...*</span>
+  ...<span>Level</span>:
+  ...
+
+In the add-form we will get a list of available courses to select
+from. This list will contain all courses stored in the site, not only
+the ones from local department:
+
+  >>> ctrl = browser.getControl(name='form.course')
+  >>> ctrl.displayOptions
+  ['--', 'COURSE1 - Course 1', 'COURSE2 - Course 2']
+
+We select the first course and create our certificate course:
+
+  >>> ctrl.getControl('COURSE1').selected = True
+  >>> browser.getControl(name='form.level').value = ['100']
+  >>> browser.getControl('Add certificate course').click()
+
+Our certificate course will be linked on the parent certificate page:
+
+  >>> browser.open(cert_url)
+  >>> browser.getLink('COURSE1_100').click()
+  >>> browser.url
+  'http://localhost/my...sity/faculties/TF/TD/certificates/CERT1/COURSE1_100'
+
+We can't add the same certificate course twice:
+
+  >>> cert_url = dept_url + '/certificates/CERT1'
+  >>> browser.open(cert_url + '/manage')
+  >>> browser.getControl('Add certificate course').click()
+  >>> ctrl = browser.getControl(name='form.course')
+  >>> ctrl.getControl('COURSE1').selected = True
+  >>> browser.getControl(name='form.level').value = ['100']
+  >>> browser.getControl('Add certificate course').click()
+  >>> 'The chosen certificate course is already' in browser.contents
+  True
+
+When we started to add a new certificate course, we can also cancel the
+process before submitting. This will bring us back to the certificate
+page:
+
+  >>> browser.open(cert_url + '/addcertificatecourse')
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myuniversity/faculties/TF/TD/certificates/CERT1'
+
+
+Modifying certificate courses
+-----------------------------
+
+We can change the settings for a certcourse by clicking on the
+provided 'Edit certificate course' link:
+
+  >>> browser.open(cert_url + '/COURSE1_100')
+  >>> browser.getLink('Edit certificate course').click()
+
+If we just click 'Save and return' nothing will change:
+
+  >>> browser.getControl("Save").click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_100/@@manage'
+
+Let's set a new level (it was 100 before) and save the form. The entire
+certificate course will be replaced and we will be redirected to manage
+page afterwards:
+
+  >>> browser.getControl(name='form.level').value = ['200']
+  >>> browser.getControl("Save").click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_200/@@manage'
+
+Nothing will be changed if click ``Cancel``:
+
+  >>> browser.getControl(name='form.level').value = ['400']
+  >>> browser.getControl('Cancel').click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_200'
+
+Searching certificate courses
+-----------------------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "COURSE1"
+  >>> browser.getControl('Search').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...faculties/TF/TD/certificates/CERT1/COURSE1_200">COURSE1</a>
+  ...
+
+  >>> browser.getControl(name='query').value = "*"
+  >>> browser.getControl('Search').click()
+  >>> 'Search string not allowed' in browser.contents
+  True
+
+Deleting certificate courses
+----------------------------
+
+We can delete certcourses by browsing the containing certificate manage page:
+
+  >>> browser.open(cert_url + '/manage#tab-2')
+  >>> 'COURSE1_200</a>' in browser.contents
+  True
+  >>> browser.getControl('Remove selected certificate courses').click()
+  >>> 'No item selected' in browser.contents
+  True
+  >>> browser.getControl(name='val_id').getControl(
+  ...    value='COURSE1_200').selected = True
+  >>> browser.getControl('Remove selected certificate courses').click()
+  >>> 'Successfully removed: COURSE1_200' in browser.contents
+  True
+
+  >>> 'COURSE1_200</a>' in browser.contents
+  False
+
+
+
+Data Center
+===========
+
+The data center helps us uploading files for later import or similar.
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Data Center').click()
+
+Setting the file path
+---------------------
+
+A datacenter stores files in a path in filesystem. By default this is
+a directory in the sources:
+
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...
+  <b>Storage path:</b> <span>.../parts/test/datacenter</span>
+  ...
+
+Going to 'Settings` we can change the path:
+
+  >>> browser.getLink('Edit settings').click()
+  >>> pathsetting = browser.getControl(name='newpath')
+
+We create a directory and set it as our upload path:
+
+  >>> import os
+  >>> cwd = os.getcwd()
+  >>> uploadpath = os.path.join(cwd, 'testfiles')
+  >>> os.mkdir(uploadpath)
+  >>> pathsetting.value = uploadpath
+
+And submit the form:
+
+  >>> browser.getControl(name='save').click()
+
+We clean up the set directory path, as there might be some files be
+copied some files from installation:
+
+  >>> files = os.listdir(uploadpath)
+  >>> for filename in files:
+  ...   if os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   os.unlink(os.path.join(uploadpath, filename))
+
+We also remove any existing 'accesscodes' subdir:
+
+  >>> import shutil
+  >>> for filename in files:
+  ...   if not os.path.isdir(os.path.join(uploadpath, filename)):
+  ...     continue
+  ...   if filename != 'accesscodes':
+  ...     continue
+  ...   shutil.rmtree(os.path.join(uploadpath, filename))
+
+The new upload directory is now empty, except from the logs and other
+standard dirs, which are created automatically:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'graduated', 'logs', 'unfinished']
+
+
+Uploading files
+---------------
+
+Now we can upload files. Most interesting files might be CSV files,
+that can be imported lateron. We create a CSV file containing faculty
+descriptions:
+
+  >>> open('faculties.csv', 'wb').write(
+  ... """code,title,title_prefix
+  ... FA,Arts,faculty
+  ... FS,Sciences,faculty
+  ... """)
+
+Now we can upload this file. To do this, we first go to the upload
+page:
+
+  >>> browser.getLink('Upload data').click()
+
+and enter the appropriate data in the form:
+
+  >>> filewidget = browser.getControl(name='uploadfile:file')
+  >>> filewidget
+  <Control name='uploadfile:file' type='file'>
+
+A sidenote for developers: by marking the filewidget with the
+``:file`` extension, we tell Zope to handle this field as a file
+widget.
+
+  >>> import cStringIO
+  >>> filecontents = cStringIO.StringIO(
+  ...   open('faculties.csv', 'rb').read())
+  >>> filewidget.add_file(filecontents, 'text/plain', 'myfaculties.csv')
+
+  >>> browser.getControl(name='SUBMIT').click()
+
+The file was indeed uploaded, with the current userid inserted:
+
+  >>> sorted(os.listdir(uploadpath))
+  ['deleted', 'finished', 'graduated', 'logs', 'myfaculties_zope.mgr.csv', 'unfinished']
+
+We create and upload also a CSV file containing departments:
+
+  >>> open('departments.csv', 'wb').write(
+  ... """code,title,title_prefix,faculty_code
+  ... LIT,Literature,department,FA
+  ... SOC,Sociology,department,FA
+  ... PHY,Physics,department,FS
+  ... INF,Informatics,department,FS
+  ... MAT,Math,department,FS
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('departments.csv', 'rb').read()),
+  ...   'text/plain', 'mydepartments.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing courses:
+
+  >>> open('courses.csv', 'wb').write(
+  ... """code,level,title,passmark,credits,semester,faculty,department
+  ... LI1,,Introduction to Literature I,40,2,1,FA,LIT
+  ... LI2,,Introduction to Literature II,40,2,2,FA,LIT
+  ... AN1,000,Analysis I,40,2,1,FS,MAT
+  ... AN2,000,Analysis II,40,2,2,FS,MAT
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('courses.csv', 'rb').read()),
+  ...   'text/plain', 'mycourses.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing certificates:
+
+  >>> open('certificates.csv', 'wb').write(
+  ... """code,title,faculty_code,department_code,study_mode,end_level,m_prefix,start_level,application_category
+  ... LBA,BACHELOR OF LITERATURE,FA,LIT,UG,ug_ft,500,LIT,100,basic
+  ... LMA,MASTER OF LITERATURE,FA,LIT,UG,ug_pt,500,LIT,100,cest
+  ... DME,DIPLOMA OF MATH,FS,MAT,DP,dp_ft,200,DME,100,cest
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('certificates.csv', 'rb').read()),
+  ...   'text/plain', 'mycertificates.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+We create and upload also a CSV file containing certificate courses:
+
+  >>> open('certcourses.csv', 'wb').write(
+  ... """code,faculty_code,department_code,certificate_code,level,mandatory
+  ... LI1,FA,LIT,LBA,100,True
+  ... LI2,FA,LIT,LBA,200,True
+  ... """)
+
+  >>> browser.open('http://localhost/myuniversity/datacenter/upload')
+  >>> browser.getControl(name='uploadfile:file').add_file(
+  ...   cStringIO.StringIO(open('certcourses.csv', 'rb').read()),
+  ...   'text/plain', 'mycertcourses.csv')
+  >>> browser.getControl(name='SUBMIT').click()
+
+
+Importing a CSV file
+--------------------
+
+The import of CSV files is described in batchprocessing.txt.
+
+
+Clean up:
+
+  >>> import os
+  >>> import shutil
+  >>> shutil.rmtree(uploadpath)
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/permissions.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/permissions.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/permissions.txt	(revision 16045)
@@ -0,0 +1,120 @@
+Permissions and Roles
+*********************
+
+Permissions and roles used in a Kofa portal.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Convenience Functions
+=====================
+
+:mod:`waeup.kofa` offers some convenience functions to handle security
+roles.
+
+:func:`get_all_roles`
+---------------------
+
+Gives us all roles defined in Kofa. We get tuples of
+kind
+
+  ``(<ROLE-NAME>, <ROLE>)``
+
+where ``<ROLE-NAME>`` is the name under which a role was registered
+with the ZCA (a string) and ``<ROLE>`` is the real role object.
+
+    >>> from waeup.kofa.permissions import get_all_roles
+    >>> get_all_roles()
+    <generator object...at 0x...>
+
+    >>> sorted(list(get_all_roles()))
+    [(u'waeup.ACManager', <waeup.kofa.permissions.ACManager object at 0x...]
+
+:func:`get_waeup_roles`
+-----------------------
+
+Gives us all roles, except the Kofa specific roles. We can get a list
+with or without local roles:
+
+    >>> from waeup.kofa.permissions import get_waeup_roles
+    >>> len(list(get_waeup_roles()))
+    29
+
+    >>> len(list(get_waeup_roles(also_local=True)))
+    54
+
+
+:func:`get_waeup_role_names`
+----------------------------
+
+We can get all role names defined in Kofa (except 'local'
+roles that are meant not to be assigned globally):
+
+    >>> from waeup.kofa.permissions import get_waeup_role_names
+    >>> list(get_waeup_role_names())
+    [u'waeup.ACManager',
+     u'waeup.AcademicsManager',
+     u'waeup.AcademicsOfficer',
+     u'waeup.AccommodationOfficer',
+     u'waeup.Applicant',
+     u'waeup.ApplicationsManager',
+     u'waeup.ApplicationsOfficer',
+     u'waeup.BursaryOfficer',
+     u'waeup.DataCenterManager',
+     u'waeup.DocumentsManager',
+     u'waeup.DocumentsOfficer',
+     u'waeup.ExportManager',
+     u'waeup.FingerprintDevice',
+     u'waeup.ImportManager',
+     u'waeup.PortalManager',
+     u'waeup.ReportsManager',
+     u'waeup.ReportsOfficer',
+     u'waeup.Student',
+     u'waeup.StudentImpersonator',
+     u'waeup.StudentsClearanceOfficer',
+     u'waeup.StudentsCourseAdviser',
+     u'waeup.StudentsCreator',
+     u'waeup.StudentsManager',
+     u'waeup.StudentsOfficer',
+     u'waeup.TranscriptOfficer',
+     u'waeup.TranscriptSignee',
+     u'waeup.UsersManager',
+     u'waeup.WorkflowManager',
+     u'waeup.xmlrpcusers1']
+
+:func:`get_users_with_local_roles`
+----------------------------------
+
+We can get all users and their roles for a certain context
+object. This even works for objects that cannot have local roles as
+they are not stored in the ZODB:
+
+    >>> from waeup.kofa.permissions import get_users_with_local_roles
+    >>> mycontext = object()
+    >>> people_and_roles = get_users_with_local_roles(mycontext)
+    >>> people_and_roles
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people_and_roles = list(people_and_roles)
+    >>> people_and_roles
+    []
+
+:func:`get_users_with_role`
+---------------------------
+
+We can get all users with a specific role for a certain context
+object:
+
+    >>> from waeup.kofa.permissions import get_users_with_role
+    >>> mycontext = object()
+    >>> people = get_users_with_role('waeup.portalManager', mycontext)
+    >>> people
+    <generator object...at 0x...>
+
+In this case, the result is empty:
+
+    >>> people = list(people)
+    >>> people
+    []
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/userscontainer.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/userscontainer.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/doctests/userscontainer.txt	(revision 16045)
@@ -0,0 +1,80 @@
+Users Container
+***************
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+Before we can start, we need some password managers available:
+
+    >>> from zope.app.authentication.placelesssetup import (
+    ...   PlacelessSetup)
+    >>> PlacelessSetup().setUp()
+
+We can create a user container which will hold the useraccounts for
+us:
+
+    >>> from waeup.kofa.userscontainer import UsersContainer
+    >>> myusers = UsersContainer()
+
+We can add users, just by passing a name, a password, and (optionally)
+a title and a description:
+
+    >>> myusers.addUser('bob', 'bobssecret')
+
+Now, Bob is in the container:
+
+    >>> list(myusers)
+    [u'bob']
+
+We can get Bob's account:
+
+    >>> bob = myusers['bob']
+    >>> bob
+    <waeup.kofa.authentication.Account object at 0x...>
+
+    >>> bob.name
+    'bob'
+
+    >>> bob.title
+    'bob'
+
+    >>> bob.description
+
+
+As we did not give a title, the name was taken instead
+for title (but not for description). The password, however, is stored encoded:
+
+    >>> bob.password
+    '{SSHA}...'
+
+We can export user accounts:
+
+    >>> from waeup.kofa.userscontainer import UserExporter
+    >>> import os
+    >>> import tempfile
+    >>> workdir = tempfile.mkdtemp()
+    >>> outfile = os.path.join(workdir, 'myoutput.csv')
+    >>> exporter = UserExporter()
+    >>> site = {'users':myusers}
+    >>> exporter.export_all(site, outfile)
+    >>> result = open(outfile, 'rb').read()
+    >>> 'name,title,public_name,description,email,phone,roles,local_roles,password' in result
+    True
+    >>> '{SSHA}' in result
+    True
+
+We can delete users:
+
+    >>> myusers.delUser('alice')
+
+The container won't complain, although there is no ``alice`` stored
+yet. But we can really delete users:
+
+    >>> myusers.delUser('bob')
+    >>> list(myusers)
+    []
+
+Clean up:
+
+    >>> import shutil
+    >>> shutil.rmtree(workdir)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/__init__.py	(revision 16045)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
+from waeup.kofa.documents.container import DocumentsContainer
+from waeup.kofa.documents.document import Document
+
+__all__ = [
+    'DocumentsContainer',
+    'Document',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/batching.py	(revision 16045)
@@ -0,0 +1,170 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""Batch processing components for document objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.kofa.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IGNORE_MARKER)
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+from waeup.kofa.utils.batching import BatchProcessor
+
+
+class DocumentProcessorBase(BatchProcessor):
+    """This is the base class for all kinds of document processors.
+    The `checkConversion` method checks whether `class_name` in a row
+    corresponds with the processor chosen. This is to avoid accidentally
+    wrong imports.
+
+    Document processors do not import workflow states or transitions which
+    means, all imported documents will be unpublished after batch creation.
+    In other words, publishing can't be done by import, it has do be done
+    via the UI.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    util_name = None
+    name = None
+    iface = None
+
+    location_fields = ['document_id',]
+    additional_fields = ['class_name',]
+
+    factory_name = None
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.additional_fields +
+                    getFields(self.iface).keys())))
+
+    def parentsExist(self, row, site):
+        return 'documents' in site.keys()
+
+    def entryExists(self, row, site):
+        document_id = row.get('document_id', None)
+        cat = queryUtility(ICatalog, name='documents_catalog')
+        results = list(cat.searchResults(document_id=(document_id, document_id)))
+        if results:
+            return True
+        return False
+
+    def getParent(self, row, site):
+        return site['documents']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['document_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addDocument(obj)
+        return
+
+    def delEntry(self, row, site):
+        document = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if document is not None:
+            grok.getSite().logger.info(
+                '%s - Document removed' % document.document_id)
+            del parent[document.document_id]
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(DocumentProcessorBase, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite().logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            DocumentProcessorBase, self).checkConversion(row, mode=mode)
+        # We need to check if the class_name corresponds with the
+        # processor chosen. This is to avoid accidentally wrong imports.
+        if mode == 'create':
+            class_name = row.get('class_name', None)
+            if class_name != self.factory_name.strip('waeup.'):
+                errs.append(('class_name','wrong processor'))
+        return errs, inv_errs, conv_dict
+
+
+class PDFDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IPDFDocument objects.
+    """
+    util_name = 'pdfdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public PDF Document Processor')
+    iface = IPDFDocument
+
+    factory_name = 'waeup.PDFDocument'
+
+
+class HTMLDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IHTMLDocument objects.
+    """
+    util_name = 'htmldocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public HTML Document Processor')
+    iface = IHTMLDocument
+
+    factory_name = 'waeup.HTMLDocument'
+
+
+class RESTDocumentProcessor(DocumentProcessorBase):
+    """A batch processor for IRESTDocument objects.
+    """
+    util_name = 'restdocumentprocessor'
+    grok.name(util_name)
+
+    name = _('Public REST Document Processor')
+    iface = IRESTDocument
+
+    factory_name = 'waeup.RESTDocument'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser.py	(revision 16045)
@@ -0,0 +1,383 @@
+## $Id$
+##
+## Copyright (C) 2014 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 documents offered for download by the company.
+"""
+
+import sys
+import grok
+from urllib import urlencode
+from datetime import datetime
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.security import checkPermission
+from waeup.kofa.utils.helpers import html2dict, rest2dict
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import (
+    IContactForm, IKofaObject, IKofaUtils, DOCLINK)
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    NullValidator, jsaction, action, UtilityView)
+from waeup.kofa.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.pages import (
+    delSubobjects, add_local_role, del_local_roles, msave,
+    LocalRoleAssignmentUtilityView)
+
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument,
+    IHTMLDocument, IRESTDocument, IDocumentsUtils)
+from waeup.kofa.documents.workflow import PUBLISHED
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customers container.
+    """
+    grok.context(IDocumentsContainer)
+    title = _('Documents')
+
+
+class DocumentBreadcrumb(Breadcrumb):
+    """A breadcrumb for the customer container.
+    """
+    grok.context(IPublicDocument)
+
+    def title(self):
+        return self.context.title
+
+
+class DocumentsContainerPage(KofaDisplayFormPage):
+    """The standard view for document containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('containerpage')
+    pnav = 2
+    label = _('Documents')
+    doclink = DOCLINK + '/documents.html'
+
+
+class DocumentsContainerManageFormPage(KofaEditFormPage):
+    """The manage page for customer containers.
+    """
+    grok.context(IDocumentsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('containermanagepage')
+    pnav = 2
+    label = _('Manage document section')
+
+    @action(_('Add document'), validator=NullValidator, style='primary')
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, 'adddoc'))
+        return
+
+    @jsaction(_('Remove selected documents'))
+    def delDocuments(self, **data):
+        delSubobjects(self, redirect='manage', tab='2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class DocumentAddFormPage(KofaAddFormPage):
+    """Add-form to add a customer.
+    """
+    grok.context(IDocumentsContainer)
+    grok.require('waeup.manageDocuments')
+    grok.name('adddoc')
+    grok.template('documentaddform')
+    label = _('Add document')
+    pnav = 2
+
+    form_fields = grok.AutoFields(IPublicDocument)
+
+    @property
+    def selectable_doctypes(self):
+        doctypes = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT
+        return sorted(doctypes.items())
+
+    @action(_('Add document'), style='primary')
+    def createDocument(self, **data):
+        form = self.request.form
+        doctype = form.get('doctype', None)
+        # Here we can create various instances of PublicDocument derived
+        # classes depending on the doctype parameter given in form.
+        document = createObject('waeup.%s' % doctype)
+        self.applyData(document, **data)
+        try:
+            self.context.addDocument(document)
+        except KeyError:
+            self.flash(_('The id chosen already exists.'),
+                       type='danger')
+            return
+        doctype = getUtility(IDocumentsUtils).SELECTABLE_DOCTYPES_DICT[doctype]
+        self.flash(_('${a} added.', mapping = {'a': doctype}))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.__parent__.logger.info(
+            '%s - added: %s %s' % (ob_class, doctype, document.document_id))
+        self.redirect(self.url(self.context) +
+            '/%s/manage' % document.document_id)
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+
+class DocumentDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('index')
+    grok.require('waeup.viewDocuments')
+    grok.template('documentpage')
+    pnav = 2
+    doclink = DOCLINK + '/documents.html'
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface)
+
+    @property
+    def label(self):
+        return self.context.title
+
+
+class HTMLDocumentDisplayFormPage(DocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentpage')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'html_multilingual')
+
+    @property
+    def html(self):
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class HTMLDocumentDisplayContentPage(KofaPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IHTMLDocument)
+    grok.name('display')
+    grok.template('htmldisplaypage')
+    grok.require('waeup.Public')
+
+    @property
+    def label(self):
+        return self.context.title
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        super(HTMLDocumentDisplayContentPage, self).update()
+        return
+
+    @property
+    def content(self):
+        lang = self.request.cookies.get('kofa.language')
+        html = self.context.html_dict.get(lang,'')
+        if html =='':
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            html = self.context.html_dict.get(portal_language,'')
+        return html
+
+
+class RESTDocumentDisplayFormPage(HTMLDocumentDisplayFormPage):
+    """ Page to display html document data
+    """
+    grok.context(IRESTDocument)
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(self.context.form_fields_interface).omit(
+            'html_dict', 'rest_multilingual')
+
+
+class RESTDocumentDisplayContentPage(HTMLDocumentDisplayContentPage):
+    """ Page to display the html content of document
+    """
+    grok.context(IRESTDocument)
+
+    label = None
+
+
+class DocumentManageFormPage(KofaEditFormPage,
+                            LocalRoleAssignmentUtilityView):
+    """ View to manage document data
+    """
+    grok.context(IPublicDocument)
+    grok.name('manage')
+    grok.require('waeup.manageDocuments')
+    grok.template('documentmanagepage')
+    pnav = 2
+
+    taboneactions = [_('Save'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+
+    deletion_warning = _('Are you sure?')
+
+    @property
+    def form_fields(self):
+        return grok.AutoFields(
+            self.context.form_fields_interface).omit('document_id')
+
+    def label(self):
+        return _('Manage document ') + self.context.document_id
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        return msave(self, **data)
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+
+class HTMLDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage htmldocument data
+    """
+    grok.context(IHTMLDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'html_multilingual', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = html2dict(html_multilingual, portal_language)
+        return
+
+    # Actions must be repeated. They are not inherited from the parent class.
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class RESTDocumentManageFormPage(DocumentManageFormPage):
+    """ View to manage restdocument data
+    """
+    grok.context(IRESTDocument)
+    grok.template('htmldocumentmanagepage')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        html_multilingual = getattr(self.context, 'rest_multilingual', None)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.context.html_dict = rest2dict(html_multilingual, portal_language)
+        return
+
+    # Actions must be repeated. They are not inherited from the parent class.
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Add local role'), validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3,**data)
+
+    @action(_('Remove selected local roles'))
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class DocumentTriggerTransitionFormPage(KofaEditFormPage):
+    """ View to trigger public document transitions
+    """
+    grok.context(IPublicDocument)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger document transition')
+    pnav = 2
+
+    def update(self):
+        return super(KofaEditFormPage, self).update()
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Apply now'), style='primary')
+    def apply(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            try:
+                wf_info.fireTransition(transition_id)
+                self.flash(_("Transition '%s' executed." % transition_id))
+            except InvalidTransitionError, error:
+                self.flash(error, type="warning")
+            self.redirect(self.url(self.context))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containermanagepage.pt	(revision 16045)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST" enctype="multipart/form-data">
+  <h3 i18n:translate="">Documents</h3>
+  <table class="kofa-data-table dataTableManage">
+    <thead>
+      <tr>
+        <th>&nbsp;
+        </th>
+        <th i18n:translate="">Id</th>
+        <th i18n:translate="">Title</th>
+        <th i18n:translate="">Type</th>
+        <th i18n:translate="">State</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="doc context/values"> <td>
+          <input type="checkbox" name="val_id"
+                 tal:attributes="value doc/document_id" /> </td> <td>
+          <a tal:attributes="href python: view.url(doc)"
+             tal:content="doc/__name__"> ID</a> </td>
+          <td tal:content="doc/title">TITLE</td>
+          <td tal:content="doc/translated_class_name">CLASSNAME</td>
+          <td tal:content="doc/translated_state">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions" tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/containerpage.pt	(revision 16045)
@@ -0,0 +1,24 @@
+<div i18n:domain="waeup.kofa" i18n:translate="" tal:condition="python: not len(context.keys())">
+There are no subobjects registered yet.
+</div>
+
+<table class="kofa-data-table dataTable" i18n:domain="waeup.kofa">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Type</th>
+      <th i18n:translate="">State</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href python: view.url(value)">
+          <span tal:content="value/document_id">ID</span></a>
+      </td>
+      <td tal:content="value/title">TITLE</td>
+      <td tal:content="value/translated_class_name">CLASSNAME</td>
+      <td tal:content="value/translated_state">STATE</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentaddform.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentaddform.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentaddform.pt	(revision 16045)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span i18n:translate="">Document Type</span>:
+        </td>
+        <td>
+          <select id="doctype" name="doctype" size="1"
+                  class="form-control half">
+            <option tal:repeat="item view/selectable_doctypes"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">DOCTYPE</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentmanagepage.pt	(revision 16045)
@@ -0,0 +1,99 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">Files</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/documentpage.pt	(revision 16045)
@@ -0,0 +1,31 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.kofa">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.kofa"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="python: files.replace('\n','')">
+      <td class="separator" colspan=2 i18n:translate="">
+        Connected Files
+      </td>
+    </tr>
+    <tal:files content="structure files" />
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt	(revision 16045)
@@ -0,0 +1,88 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">Metadata</a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="tab-pane active">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab3" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">User Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+            <th i18n:translate="">Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles"> <td>
+            <input type="checkbox" name="role_id"
+                   tal:attributes="value python: entry['user_name']+'|'+entry['local_role']" /> </td>
+            <td tal:content="entry/user_name"> USERNAME </td>
+            <td tal:content="entry/user_title"> USERTITLE </td>
+            <td tal:content="entry/local_role_title"> LOCAL ROLE </td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabthreeactions1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div><br /><br />
+      <div class="form-inline">
+        <br />
+        <div class="form-group">
+          <select id="user" name="user" class="form-control">
+            <option tal:repeat="user view/getUsers"
+                    tal:attributes="value user/name">
+            <span tal:replace="user/val">USERSTRING</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <select id="local_role" name="local_role" class="form-control">
+            <option tal:repeat="localrole view/getLocalRoles"
+                    tal:attributes="value localrole/name">
+            <span tal:replace="localrole/title">LOCALROLETITLE</span>
+            </option>
+          </select>
+        </div>
+        <div class="form-group">
+          <div tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                  <input tal:condition="python:action.label in view.tabthreeactions2"
+                         tal:replace="structure action/render"/>
+                </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/htmldocumentpage.pt	(revision 16045)
@@ -0,0 +1,43 @@
+<div class="wfstatus-sub"  i18n:domain="waeup.kofa">
+  Document State: <span tal:replace="context/translated_state">
+    STATE</span>
+</div>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<table class="form-table" i18n:domain="waeup.kofa"
+  tal:define="files provider:files">
+  <tbody>
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Document Type:
+      </td>
+      <td>
+        <span tal:content="context/translated_class_name">CLASSNAME</span>
+      </td>
+    </tr>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td i18n:translate="" class="fieldname">
+        Content:
+      </td>
+      <td>
+        <span tal:content="structure view/html">HTML</span>
+      </td>
+    </tr>
+    <tr tal:condition="python: getattr(context, 'state', None) == 'published'">
+      <td i18n:translate="" class="fieldname">
+        HTML Element:
+      </td>
+      <td>
+        &lt;a href=&quot;<span tal:replace="python: view.url(context, 'display')"></span>&quot;&gt;<span tal:replace="context/title">
+        FILENAME
+        </span>&lt;/a&gt;
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/trigtrans.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/trigtrans.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/browser_templates/trigtrans.pt	(revision 16045)
@@ -0,0 +1,19 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+    <br />
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/catalog.py	(revision 16045)
@@ -0,0 +1,32 @@
+## $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
+##
+"""Components to help cataloging and searching documents.
+"""
+import grok
+from waeup.kofa.interfaces import IUniversity
+from waeup.kofa.documents.interfaces import IDocument
+
+class DocumentsCatalog(grok.Indexes):
+    """A catalog for all documents.
+    """
+    grok.site(IUniversity)
+    grok.name('documents_catalog')
+    grok.context(IDocument)
+
+    document_id = grok.index.Field(attribute='document_id')
+    state = grok.index.Field(attribute='state')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/container.py	(revision 16045)
@@ -0,0 +1,60 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""
+Containers which contain document objects.
+"""
+import grok
+from grok import index
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.documents.interfaces import IDocumentsContainer, IDocument
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.utils.logger import Logger
+
+class DocumentsContainer(grok.Container, Logger):
+    """This is a container for all kind of documents.
+    """
+    grok.implements(IDocumentsContainer)
+    grok.provides(IDocumentsContainer)
+
+
+    def addDocument(self, document):
+        if not IDocument.providedBy(document):
+            raise TypeError(
+                'DocumentsContainers contain only IDocument instances')
+        self[document.document_id] = document
+        return
+
+DocumentsContainer = attrs_to_fields(DocumentsContainer)
+
+class DocumentsPlugin(grok.GlobalUtility):
+    """A plugin that creates container for documents inside a company.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('documents')
+
+    def setup(self, site, name, logger):
+        if 'documents' in site.keys():
+            logger.warn('Could not create container for documents.')
+            return
+        site['documents'] = DocumentsContainer()
+        logger.info('Container for documents created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'documents' in site.keys():
+            self.setup(site, name, logger)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/document.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/document.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/document.py	(revision 16045)
@@ -0,0 +1,319 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""
+These are the public documents.
+"""
+import os
+import grok
+from grok import index
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from zope.event import notify
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.i18n import translate
+
+from waeup.kofa.image import KofaImageFile
+from waeup.kofa.imagestorage import DefaultFileStoreHandler
+from waeup.kofa.interfaces import (
+    IKofaUtils, IObjectHistory,
+    IFileStoreNameChooser, IFileStoreHandler, IExtFileStore)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import attrs_to_fields, get_current_principal
+from waeup.kofa.documents.interfaces import (
+    IDocument, IPublicDocument, IDocumentsUtils,
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+
+@grok.subscribe(IDocument, grok.IObjectRemovedEvent)
+def handle_document_removed(document, event):
+    store = getUtility(IExtFileStore)
+    for filename in document.filenames:
+        store.deleteFileByContext(document, attr=filename)
+    return
+
+
+class Document(grok.Container):
+    """This is a document.
+    """
+    grok.implements(IDocument)
+    grok.provides(IDocument)
+    grok.baseclass()
+
+    form_fields_interface = None
+
+    # Kofa can store any number of files per Document object.
+    # However, we highly recommend to associate and store
+    # only one file per Document object. Thus the following
+    # tuple should contain only a single filename string.
+    filenames = ()
+
+    local_roles = [
+        'waeup.local.DocumentManager',
+        ]
+
+    state = None
+    translated_state = None
+    translated_class_name = None
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def class_name(self):
+        return self.__class__.__name__
+
+    @property
+    def formatted_transition_date(self):
+        try:
+            return self.history.messages[-1].split(' - ')[0]
+        except IndexError:
+            return
+
+class PublicDocumentBase(Document):
+    """This is a public document baseclass.
+    """
+    grok.implements(IPublicDocument)
+    grok.provides(IPublicDocument)
+    grok.baseclass()
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            TRANSLATED_STATES = getUtility(
+                IDocumentsUtils).TRANSLATED_DOCUMENT_STATES
+            return TRANSLATED_STATES[self.state]
+        except KeyError:
+            return
+
+    @property
+    def translated_class_name(self):
+        try:
+            DOCTYPES_DICT = getUtility(IDocumentsUtils).DOCTYPES_DICT
+            return DOCTYPES_DICT[self.class_name]
+        except KeyError:
+            return
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+
+class PDFDocument(PublicDocumentBase):
+    """This is a  document for a single pdf upload file.
+    """
+    grok.implements(IPDFDocument)
+    grok.provides(IPDFDocument)
+
+    filenames = ('file.pdf',)
+
+    form_fields_interface = IPDFDocument
+
+PDFDocument = attrs_to_fields(PDFDocument)
+
+
+class HTMLDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IHTMLDocument)
+    grok.provides(IHTMLDocument)
+
+    form_fields_interface = IHTMLDocument
+
+    def __init__(self, *args, **kw):
+        super(HTMLDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+HTMLDocument = attrs_to_fields(HTMLDocument)
+
+
+class RESTDocument(PublicDocumentBase):
+    """This is a  document to render html-coded text.
+    """
+    grok.implements(IRESTDocument)
+    grok.provides(IRESTDocument)
+
+    form_fields_interface = IRESTDocument
+
+    def __init__(self, *args, **kw):
+        super(RESTDocument, self).__init__(*args, **kw)
+        self.html_dict = {}
+
+RESTDocument = attrs_to_fields(RESTDocument)
+
+
+class PDFDocumentFactory(grok.GlobalUtility):
+    """A factory for documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.PDFDocument')
+    title = u"Create a new PDF document.",
+    description = u"This factory instantiates new PDF documents."
+
+    def __call__(self, *args, **kw):
+        return PDFDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(PDFDocument)
+
+
+class HTMLDocumentFactory(grok.GlobalUtility):
+    """A factory for HTML documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.HTMLDocument')
+    title = u"Create a new HTML document.",
+    description = u"This factory instantiates new HTML documents."
+
+    def __call__(self, *args, **kw):
+        return HTMLDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(HTMLDocument)
+
+
+class RESTDocumentFactory(grok.GlobalUtility):
+    """A factory for REST documents.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.RESTDocument')
+    title = u"Create a new REST document.",
+    description = u"This factory instantiates new REST documents."
+
+    def __call__(self, *args, **kw):
+        return RESTDocument(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(RESTDocument)
+
+
+#: The file id marker for files
+DOCUMENT_FILE_STORE_NAME = 'file-document'
+
+
+class DocumentFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Document` objects.
+
+    `context` is a :class:`Document` instance.
+
+    The delivered file_id contains the file id marker for
+    :class:`Document` objects in the central :class:`DocumentsContainer`.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for customer document
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for customer documents.
+
+    .. seealso:: :mod:`waeup.kofa.imagestorage`
+
+    """
+
+    grok.context(IDocument)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for customer document context.
+
+        *Example:*
+
+        For document with id ``d123`` and
+        with attr ``nice_image.jpeg`` this chooser would create:
+
+          ``__file-document__nice_image_d123.jpeg``
+
+        meaning that the nice image of this document would be
+        stored in the site-wide file storage in path:
+
+          ``nice_image_d123.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        doc_id = self.context.document_id
+        marked_filename = '__%s__%s_%s%s' % (
+            DOCUMENT_FILE_STORE_NAME,
+            basename, doc_id, ext)
+        return marked_filename
+
+
+class DocumentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """ Document specific file handling.
+
+    This handler knows in which path in a filestore to store document
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-document__`` (the marker string for customer files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(DOCUMENT_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All document files are put in directory ``documents``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'documents')
+        return super(DocumentFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj = super(
+            DocumentFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, KofaImageFile(
+            file_obj.filename, file_obj.data)
+
+
+@grok.subscribe(IDocument, grok.IObjectAddedEvent)
+def handle_document_added(document, event):
+    """If a document is added the transition create is fired.
+    The latter produces a logging message.
+    """
+    if IWorkflowState(document).getState() is None:
+        IWorkflowInfo(document).fireTransition('create')
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/export.py	(revision 16045)
@@ -0,0 +1,120 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for documents.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names
+from waeup.kofa.documents.interfaces import (
+    IPDFDocument, IHTMLDocument, IRESTDocument)
+
+
+class DocumentExporterBase(grok.GlobalUtility, ExporterBase):
+    """This is the exporter base class for all kinds of document exporters.
+    Derived classes export all documents of type `class_name` inside the
+    documents container.
+
+    All schema fields and the property attributes
+    `history`, `state`, `class_name` and `users_with_local_roles`
+    are being exported.
+    """
+    grok.implements(ICSVExporter)
+    grok.baseclass()
+    class_name = None
+    iface = None
+    title = None
+
+    @property
+    def fields(self):
+        return tuple(sorted(iface_names(self.iface, exclude_attribs=False,
+            omit=['translated_state',
+                  'formatted_transition_date',
+                  'translated_class_name',
+                  'form_fields_interface',
+                  'filenames',
+                  'local_roles',
+                  ]
+            ))) + (
+            'users_with_local_roles',)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler prepares the history messages and computes the
+        `users_with_local_roles` value which is a Python expression like:
+
+        ``[{'user_name': u'bob', 'local_role': u'bobsrole'},
+        {'user_name': u'anna', 'local_role': u'annasrole'}]``
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        if name == 'history':
+            value = value.messages
+        return super(DocumentExporterBase, self).mangle_value(
+            value, name, context)
+
+    def export(self, documents, filepath=None):
+        """Export `documents`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for document in documents:
+            self.write_item(document, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export documents in documentscontainer into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        documents = site.get('documents', {}).values()
+        documents = [doc for doc in documents
+                     if doc.class_name == self.class_name]
+        return self.export(documents, filepath)
+
+
+class PDFDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('pdfdocuments')
+    iface = IPDFDocument
+    class_name = 'PDFDocument'
+    title = _(u'Public PDF Documents')
+
+
+class HTMLDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('htmldocuments')
+    iface = IHTMLDocument
+    class_name = 'HTMLDocument'
+    title = _(u'Public HTML Documents')
+
+
+class RESTDocumentExporter(DocumentExporterBase):
+    """Exporter for documents.
+    """
+    grok.name('restdocuments')
+    iface = IRESTDocument
+    class_name = 'RESTDocument'
+    title = _(u'Public REST Documents')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/fileviewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/fileviewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/fileviewlets.py	(revision 16045)
@@ -0,0 +1,93 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+
+import grok
+from zope.component import getUtility
+from waeup.kofa.interfaces import IExtFileStore
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.layout import UtilityView
+from waeup.kofa.browser.fileviewlets import (
+    FileDisplay, FileUpload, Image)
+from waeup.kofa.utils.helpers import file_size
+
+from waeup.kofa.documents.document import PDFDocument
+from waeup.kofa.documents.browser import (
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+from waeup.kofa.documents.workflow import PUBLISHED
+
+
+# File viewlets for documents
+
+class PDFScanManageUpload(FileUpload):
+    """Scan upload viewlet for officers.
+    """
+    grok.view(DocumentManageFormPage)
+    grok.context(PDFDocument)
+    grok.require('waeup.manageDocuments')
+    label = _(u'PDF File')
+    title = _(u'PDF File')
+    mus = 1.5 * 1024*1024
+    download_name = u'file.pdf'
+    tab_redirect = '#tab2'
+
+    @property
+    def download_filename(self):
+        file = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        fs = file_size(file)/1024.0
+        return u"%s.pdf (%.1f kB)" % (self.context.document_id, fs)
+
+
+class PDFScanDisplay(FileDisplay):
+    """Scan display viewlet.
+    """
+    grok.order(1)
+    grok.context(PDFDocument)
+    grok.require('waeup.viewDocuments')
+    grok.view(DocumentDisplayFormPage)
+    label = _(u'PDF Scan')
+    title = _(u'PDF Scan')
+    download_name = u'file.pdf'
+
+    @property
+    def download_filename(self):
+        file = getUtility(IExtFileStore).getFileByContext(
+            self.context, attr=self.download_name)
+        fs = file_size(file)/1024.0
+        return u"%s.pdf (%.1f kB)" % (self.context.document_id, fs)
+
+
+class PDFScanImage(UtilityView, Image):
+    """Scan document.
+    """
+    grok.name('file.pdf')
+    grok.context(PDFDocument)
+    grok.require('waeup.Public')
+    download_name = u'file.pdf'
+
+    @property
+    def download_filename(self):
+        return self.context.document_id
+
+    def update(self):
+        if self.context.state != PUBLISHED:
+            self.flash(_('The document requested has not yet been published.'),
+                type="warning")
+            self.redirect(self.application_url())
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/interfaces.py	(revision 16045)
@@ -0,0 +1,103 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+from zope.interface import Attribute, Interface
+from zope import schema
+from waeup.kofa.interfaces import (
+    IKofaObject, validate_id, validate_html,
+    ContextualDictSourceFactoryBase)
+from waeup.kofa.interfaces import MessageFactory as _
+
+
+class IDocumentsContainer(IKofaObject):
+    """A container for all kind of document objects.
+    """
+
+    def addDocument(document):
+        """Add a document.
+        """
+
+
+class IDocument(IKofaObject):
+    """A base representation of documents.
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Workflow state of a document')
+    class_name = Attribute('Name of the document class')
+    local_roles = Attribute('List of local role names')
+    translated_class_name = Attribute('Translatable class name of a document')
+    translated_state = Attribute('Translatable workflow state of a document')
+    formatted_transition_date = Attribute(
+        'Last transition formatted date string')
+    form_fields_interface = Attribute(
+        'Interface which is used in forms for grok.AutoFields')
+
+    document_id = schema.TextLine(
+        title = _(u'Document Id'),
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Document Title'),
+        required = True,
+        )
+
+class IPublicDocument(IDocument):
+    """A base representation of public documents.
+    """
+
+    def writeLogMessage(view, message):
+        """Add an INFO message to main.log.
+        """
+
+class IPDFDocument(IPublicDocument):
+    """A base representation of PDF documents.
+    """
+
+    filenames = Attribute('Tuple of filenames; only one filename recommended')
+
+class IHTMLDocument(IPublicDocument):
+    """A base representation of HTML documents.
+    """
+
+    html_dict = Attribute(
+        'Content as language dictionary with values in HTML format')
+
+    html_multilingual = schema.Text(
+        title = _(u'Multilingual content in HTML format'),
+        required = False,
+        constraint=validate_html,
+        )
+
+class IRESTDocument(IPublicDocument):
+    """A base representation of REST documents.
+    """
+
+    html_dict = Attribute(
+        'Content as language dictionary with values in HTML format')
+
+    rest_multilingual = schema.Text(
+        title = _(u'Multilingual content in REST (reStructuredText) format'),
+        required = False,
+        )
+
+
+class IDocumentsUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/permissions.py	(revision 16045)
@@ -0,0 +1,51 @@
+## $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
+##
+import grok
+
+# Permissions
+
+class ViewDocuments(grok.Permission):
+    grok.name('waeup.viewDocuments')
+
+
+class ManageDocuments(grok.Permission):
+    grok.name('waeup.manageDocuments')
+
+
+# Local Roles
+
+class DocumentManager(grok.Role):
+    grok.name('waeup.local.DocumentManager')
+    grok.title(u'Document Manager')
+    grok.permissions('waeup.manageDocuments',
+                     'waeup.viewDocuments',
+                     'waeup.exportData')
+
+
+# Site Roles
+class DocumentsOfficer(grok.Role):
+    grok.name('waeup.DocumentsOfficer')
+    grok.title(u'Documents Officer (view only)')
+    grok.permissions('waeup.viewDocuments')
+
+
+class DocumentsManager(grok.Role):
+    grok.name('waeup.DocumentsManager')
+    grok.title(u'Documents Manager')
+    grok.permissions('waeup.viewDocuments',
+                     'waeup.manageDocuments')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/__init__.py	(revision 16045)
@@ -0,0 +1,3 @@
+"""This package contains everything regarding documents.
+"""
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data.csv	(revision 16045)
@@ -0,0 +1,6 @@
+document_id,title,class_name
+ABC,ABC title,PDFDocument
+CDE,CDE title,PDFDocument
+EFG,EFG title,PDFDocument
+ABC,Duplicate document,PDFDocument
+HIJ,HIJ title,HTMLDocument
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/sample_document_data_update.csv	(revision 16045)
@@ -0,0 +1,3 @@
+document_id,title
+ABC,ABC new title
+XYZ,Non-existing document
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""Unit tests for document data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from time import time
+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.interface.verify import verifyClass, verifyObject
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IBatchProcessor, FatalCSVError, IUserAccount
+from waeup.kofa.documents.batching import PDFDocumentProcessor
+from waeup.kofa.documents.document import PDFDocument, HTMLDocument
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+DOCUMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS = DOCUMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+DOCUMENT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_document_data_update.csv'),
+    'rb').read()
+
+DOCUMENT_HEADER_FIELDS_UPDATE = DOCUMENT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class DocumentImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentImportExportSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        return
+
+    def tearDown(self):
+        super(DocumentImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        document1 = PDFDocument()
+        document1.document_id = u'DOC1'
+        self.app['documents'].addDocument(document1)
+        self.pdfdocument = document1
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document1)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+
+        document2 = HTMLDocument()
+        document2.document_id = u'DOC2'
+        self.app['documents'].addDocument(document2)
+        self.htmldocument = document2
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        role_manager = IPrincipalRoleManager(document2)
+        role_manager.assignRoleToPrincipal(u'johnsrole', u'john')
+        return
+
+
+class PDFDocumentProcessorTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentProcessorTest, self).setUp()
+
+        # Add document
+        document = PDFDocument()
+        document.document_id = u'DOC1'
+        self.app['documents'].addDocument(document)
+        document.title = u'Our PDF Document'
+        notify(grok.ObjectModifiedEvent(document))
+        self.document = self.app['documents'][document.document_id]
+
+        self.processor = PDFDocumentProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_document_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_document_data_update.csv')
+        open(self.csv_file, 'wb').write(DOCUMENT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(DOCUMENT_SAMPLE_DATA_UPDATE)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, PDFDocumentProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(document_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(document_id=self.document.document_id), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['documents']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(document_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(document_id=self.document.document_id), self.app) is self.document
+
+    def test_addEntry(self):
+        new_document = PDFDocument()
+        new_document.document_id = u'DOC2'
+        self.processor.addEntry(
+            new_document, {}, self.app)
+        assert len(self.app['documents'].keys()) == 2
+        self.assertEqual(self.app['documents']['DOC2'].document_id, 'DOC2')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='PDFDocument'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='id with spaces', class_name='PDFDocument'))
+        self.assertEqual(len(errs),1)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(document_id='DOC4', class_name='WrongDocument'), mode='create')
+        self.assertEqual(len(errs),1)
+
+    def test_delEntry(self):
+        assert self.document.document_id in self.app['documents'].keys()
+        self.processor.delEntry(
+            dict(document_id=self.document.document_id), self.app)
+        assert self.document.document_id not in self.app['documents'].keys()
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        self.assertEqual(num_warns,2)
+        self.assertEqual(len(self.app['documents']), 4)
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data - '
+            'EFG - updated: document_id=EFG, title=EFG title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'PDFDocument,ABC,Duplicate document,This object already exists.'
+            in failcontent)
+        self.assertTrue(
+            'HTMLDocument,HIJ,HIJ title,class_name: wrong processor'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, DOCUMENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, DOCUMENT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,1)
+        # title has changed
+        self.assertEqual(self.app['documents']['ABC'].title,'ABC new title')
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Public PDF Document Processor - sample_document_data_update - '
+            'ABC - updated: document_id=ABC, title=ABC new title\n'
+            in logcontent)
+        failcontent = open(fail_file).read()
+        self.assertTrue(
+            'XYZ,Non-existing document,Cannot update: no such entry'
+            in failcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_browser.py	(revision 16045)
@@ -0,0 +1,372 @@
+## $Id$
+## 
+## Copyright (C) 2014 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 customer-related UI components.
+"""
+import shutil
+import tempfile
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import (
+    IUserAccount, IJobManager,
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler)
+from waeup.kofa.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.documents.workflow import PUBLISHED
+
+PH_LEN = 15911  # Length of placeholder file
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+#SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+SAMPLE_PDF = os.path.join(os.path.dirname(__file__), 'test_pdf.pdf')
+
+class FullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for customers handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a company etc.
+    # so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/documents'
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(FullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+
+class DocumentUITests(FullSetup):
+    # Tests for document related views and pages
+
+    def test_manage_file_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['PDFDocument']
+        self.browser.getControl(name="form.title").value = 'My PDF Document'
+        self.browser.getControl(name="form.document_id").value = 'DOC1'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('PDF Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC1']
+
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My first doc'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC1/index')
+
+        # File can be uploaded
+        self.browser.getLink("Manage").click()
+        # Create a pseudo image file and select it to be uploaded
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.jpg')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'pdf file format expected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        self.assertTrue(
+            'Could not determine file type' in self.browser.contents)
+        pdf = open(SAMPLE_PDF, 'rb')
+        ctrl = self.browser.getControl(name='pdfscanmanageupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pdf, filename='my_sample_scan.pdf')
+        self.browser.getControl(
+            name='upload_pdfscanmanageupload').click()
+        # The file size is shown
+        self.assertTrue(
+            'href="http://localhost/app/documents/DOC1/file.pdf">DOC1.pdf (23.7 kB)</a>'
+            in self.browser.contents)
+        # Also on display page the file size is displayed.
+        self.browser.open(self.container_path + '/DOC1')
+        self.assertTrue(
+            'href="http://localhost/app/documents/DOC1/file.pdf">DOC1.pdf (23.7 kB)</a>'
+            in self.browser.contents)
+        # The file can be found in the file system
+        file = getUtility(IExtFileStore).getFileByContext(
+            document, attr='file.pdf')
+        file_content = file.read()
+        pdf.seek(0)
+        pdf_content = pdf.read()
+        self.assertEqual(file_content, pdf_content)
+        # Browsing the link shows a real pdf only if the document
+        # has been published
+        self.browser.getLink("DOC1.pdf").click()
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC1/file.pdf')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'application/pdf')
+        # The name of the downloaded file will be different
+        self.assertEqual(
+            self.browser.headers['Content-Disposition'],
+            'attachment; filename="DOC1.pdf')
+
+        # Transitions can be performed
+        self.assertEqual(document.state, 'published')
+        self.browser.open(self.container_path + '/DOC1')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['retract']
+        self.browser.getControl("Apply now").click()
+        self.assertEqual(document.state, 'created')
+
+        # Documents can be removed
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # File has been removed too
+        file = getUtility(IExtFileStore).getFileByContext(
+            document, attr='file.pdf')
+        self.assertTrue(file is None)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: PDF Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - saved: title'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentManageFormPage - %s - uploaded: file.pdf (my_sample_scan.pdf)'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document retracted' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_html_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['HTMLDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC2'
+        self.browser.getControl(name="form.title").value = 'My HTML Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('HTML Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC2']
+        # All form action buttons are there
+        self.assertTrue('value="Add local role"' in self.browser.contents)
+        self.assertTrue('value="Save"' in self.browser.contents)
+        self.assertTrue('value="Cancel"' in self.browser.contents)
+        # Document can be edited
+        self.browser.getControl(name="form.title").value = 'My second doc'
+        self.browser.getControl(name="form.html_multilingual").value = """
+<h1>Hello World</h1>
+>>de<<
+<h1>Hallo Welt</h1>
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC2/index')
+        self.assertTrue(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertFalse(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        self.browser.getLink("de", index=2).click()
+        self.assertFalse(
+            '<h1>Hello World</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+        # The content can't be rendered yet
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            'The document requested has not yet been published'
+            in self.browser.contents)
+        # We have been redirected to the portal root
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+
+        # Transitions can be performed
+        self.browser.open(self.container_path + '/DOC2')
+        self.browser.getLink("Transition").click()
+        self.browser.getControl(name="transition").value = ['publish']
+        self.browser.getControl("Apply now").click() # not yet translated
+        self.assertEqual(document.state, 'published')
+
+        # The content can be rendered
+        self.browser.open(self.container_path + '/DOC2/display')
+        self.assertTrue(
+            '<h1>Hallo Welt</h1>' in self.browser.contents)
+
+        # Documents can be removed
+        self.browser.getLink("en", index=3).click()
+        self.browser.getLink("Documents").click()
+        self.browser.getLink("Manage").click()
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=document.document_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # All actions are being logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document created' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentAddFormPage - added: HTML Document %s'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.HTMLDocumentManageFormPage - %s - saved: title + html_multilingual'
+            % document.document_id in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - %s - Document published' % document.document_id
+            in logcontent)
+        self.assertTrue(
+            'INFO - zope.mgr - documents.browser.DocumentsContainerManageFormPage - removed: %s'
+            % document.document_id in logcontent)
+
+    def test_manage_rest_document(self):
+        # Managers can access the pages of documentsconter
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Documents").click()
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Add document").click()
+        self.browser.getControl(name="doctype").value = ['RESTDocument']
+        self.browser.getControl(name="form.document_id").value = 'DOC3'
+        self.browser.getControl(name="form.title").value = 'My REST Document'
+        self.browser.getControl("Add document").click()
+        self.assertTrue('REST Document added.' in self.browser.contents)
+        document = self.app['documents']['DOC3']
+
+        # Document can be edited
+        self.browser.getControl(name="form.rest_multilingual").value = """
+----------
+Main Title
+----------
+
+Subtitle
+========
+>>de<<
+----------
+Haupttitel
+----------
+
+Untertitel
+==========
+"""
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved.' in self.browser.contents)
+        self.browser.getLink("View").click()
+        self.assertEqual(self.browser.url, self.container_path + '/DOC3/index')
+        self.assertTrue(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h2 class="subtitle" id="subtitle">Subtitle</h2>'
+            in self.browser.contents)
+        self.assertFalse(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        self.browser.getLink("de", index=2).click()
+        self.assertFalse(
+            '<h1 class="title">Main Title</h1>' in self.browser.contents)
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The content can be rendered
+        IWorkflowState(document).setState(PUBLISHED)
+        self.browser.open(self.container_path + '/DOC3/display')
+        self.assertTrue(
+            '<h1 class="title">Haupttitel</h1>' in self.browser.contents)
+        # The page label (object title) is not displayed
+        self.assertFalse(
+            '<h1 class="kofa-content-label">My REST Document</h1>'
+            in self.browser.contents)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_document.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_document.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_document.py	(revision 16045)
@@ -0,0 +1,168 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""
+Tests for documents.
+"""
+import shutil
+import tempfile
+from StringIO import StringIO
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject, queryUtility
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.kofa.interfaces import (
+    IObjectHistory, IFileStoreNameChooser, IFileStoreHandler)
+from waeup.kofa.imagestorage import DefaultStorage
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument, IPDFDocument,
+    IHTMLDocument, IRESTDocument)
+from waeup.kofa.documents.container import DocumentsContainer
+from waeup.kofa.documents.document import (
+    PDFDocument, HTMLDocument, RESTDocument,
+    DocumentFileNameChooser, DocumentFileStoreHandler)
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+
+class DocumentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IDocumentsContainer, DocumentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IDocumentsContainer, DocumentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IPDFDocument, PDFDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPDFDocument, PDFDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IHTMLDocument, HTMLDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IHTMLDocument, HTMLDocument())
+            )
+        self.assertTrue(
+            verifyClass(
+                IRESTDocument, RESTDocument)
+            )
+        self.assertTrue(
+            verifyObject(
+                IRESTDocument, RESTDocument())
+            )
+        return
+
+    def test_addDocument(self):
+        container = DocumentsContainer()
+        document = createObject(u'waeup.HTMLDocument')
+        document.document_id = u'DOC'
+        container.addDocument(document)
+        self.assertEqual(container[document.document_id], document)
+        self.assertRaises(TypeError, container.addDocument, object())
+        self.assertEqual(document.document_id, 'DOC')
+        return
+
+    def test_document_workflow(self):
+        document = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(document).fireTransition('create')
+        self.assertEqual(IWorkflowState(document).getState(), 'created')
+        return
+
+    def test_document_history(self):
+        doc = createObject(u'waeup.HTMLDocument')
+        IWorkflowInfo(doc).fireTransition('create')
+        messages = ' '.join(doc.history.messages)
+        self.assertTrue('Document created by system' in messages)
+        return
+
+
+class DocumentFileTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(DocumentFileTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(DocumentFileTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_file_store_handler_util_accessible(self):
+        # we can get an IFileStoreHandler utility for documents
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        self.assertTrue(
+            isinstance(handler, DocumentFileStoreHandler))
+        return
+
+    def test_file_store_handler(self):
+        store = DefaultStorage()
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        result = handler.pathFromFileID(
+            store, '/fake-root', '__any_marker__sample.jpg')
+        self.assertEqual(
+            result, '/fake-root/documents/sample')
+        return
+
+    def test_file_store_handler_create(self):
+        # we can create files in image store with the document file
+        # store handler
+        store = DefaultStorage(self.workdir)
+        handler = queryUtility(IFileStoreHandler, name='file-document')
+        file_obj, path, waeup_file = handler.createFile(
+            store, store.root, 'sample.jpg', '__file_document__sample',
+            StringIO('I am a JPG file'))
+        self.assertEqual(path[-20:], 'documents/sample.jpg')
+        return
+
+    def test_iface(self):
+        # make sure we implement promised interfaces
+        obj = DocumentFileNameChooser(None) # needs a context normally
+        verifyClass(IFileStoreNameChooser, DocumentFileNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_name_chooser_available(self):
+        # we can get a name chooser for document objects as adapter
+        doc = PDFDocument()
+        chooser = IFileStoreNameChooser(doc)
+        self.assertTrue(chooser is not None)
+        return
+
+    def test_name_chooser_document(self):
+        # we can get an image filename for documents not in a container
+        doc = PDFDocument()
+        doc.document_id = u'DOC'
+        chooser = IFileStoreNameChooser(doc)
+        result = chooser.chooseName('sample.jpg')
+        # the file would be stored in a ``_default`` directory.
+        self.assertEqual(
+            result, '__file-document__sample_DOC.jpg')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/tests/test_export.py	(revision 16045)
@@ -0,0 +1,138 @@
+## $Id$
+##
+## Copyright (C) 2014 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 document exporter.
+"""
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.documents.export import (
+    PDFDocumentExporter, HTMLDocumentExporter)
+from waeup.kofa.documents.document import PDFDocument, HTMLDocument
+from waeup.kofa.documents.tests.test_batching import DocumentImportExportSetup
+from waeup.kofa.testing import FunctionalLayer
+
+
+class PDFDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PDFDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = PDFDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, PDFDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="pdfdocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = PDFDocumentExporter()
+        exporter.export([self.pdfdocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = PDFDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,state,title,users_with_local_roles\r\n'
+            'PDFDocument,DOC1,'
+            '[u\'2014-12-21 16:55:16 UTC - Document created by system\'],'
+            'created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+
+class HTMLDocumentExporterTest(DocumentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(HTMLDocumentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = HTMLDocumentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, HTMLDocumentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an document exporter as utility
+        result = queryUtility(ICSVExporter, name="htmldocuments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export a document
+        exporter = HTMLDocumentExporter()
+        exporter.export([self.htmldocument], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all documents
+        exporter = HTMLDocumentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            'class_name,document_id,history,html_dict,'
+            'html_multilingual,state,title,users_with_local_roles\r\n'
+            'HTMLDocument,DOC2,'
+            '[u\'2014-12-21 16:50:28 UTC - Document created by system\'],'
+            '{},,created,,"[{\'user_name\': u\'john\', \'local_role\': u\'johnsrole\'}]"',
+            result
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/utils.py	(revision 16045)
@@ -0,0 +1,43 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""General helper functions and utilities for the documents.
+"""
+import grok
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.documents.workflow import (
+    CREATED, PUBLISHED)
+from waeup.kofa.documents.interfaces import IDocumentsUtils
+
+
+class DocumentsUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(IDocumentsUtils)
+
+    TRANSLATED_DOCUMENT_STATES = {
+        CREATED: _('created'),
+        PUBLISHED:_('published')
+        }
+
+    DOCTYPES_DICT = {
+        'PDFDocument': _('PDF Document'),
+        'HTMLDocument': _('HTML Document'),
+        'RESTDocument': _('REST Document'),
+        }
+
+    SELECTABLE_DOCTYPES_DICT = DOCTYPES_DICT
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/viewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/viewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/viewlets.py	(revision 16045)
@@ -0,0 +1,92 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""Viewlet components for documents offered for download by the company.
+"""
+
+import grok
+from waeup.kofa.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.kofa.interfaces import IKofaObject
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.viewlets import PrimaryNavTab
+
+from waeup.kofa.documents.interfaces import (
+    IDocumentsContainer, IPublicDocument)
+from waeup.kofa.documents.browser import (
+    DocumentsContainerManageFormPage, DocumentsContainerPage,
+    DocumentManageFormPage, DocumentDisplayFormPage)
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class DocumentsTab(PrimaryNavTab):
+    """Documents tab in primary navigation.
+    """
+
+    grok.context(IKofaObject)
+    grok.order(10)
+    grok.require('waeup.viewDocuments')
+    grok.name('documentstab')
+
+    pnav = 2
+    tab_title = _(u'Documents')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('documents')
+
+class DocumentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IDocumentsContainer)
+    grok.view(DocumentsContainerPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentManageFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('View')
+    target = 'index'
+    icon = 'actionicon_view.png'
+
+
+class DocumentManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    text = _('Manage')
+    target = 'manage'
+
+
+class DocumentTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IPublicDocument)
+    grok.view(DocumentDisplayFormPage)
+    grok.require('waeup.manageDocuments')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Transition')
+    target = 'trigtrans'
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/documents/workflow.py	(revision 16045)
@@ -0,0 +1,97 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""Workflow for documents.
+"""
+
+import grok
+from zope.component import getUtility
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import (
+    IWorkflowState, IWorkflowTransitionEvent, InvalidTransitionError)
+from waeup.kofa.interfaces import (
+    IObjectHistory, IKofaWorkflowInfo,
+    SimpleKofaVocabulary)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
+from waeup.kofa.documents.interfaces import IPublicDocument
+
+CREATED = 'created'
+PUBLISHED = 'published'
+
+PUBLISHING_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create document'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Document created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'publish',
+        title = _('Publish document'),
+        source = CREATED,
+        condition = NullCondition,
+        msg = _('Document published'),
+        destination = PUBLISHED),
+
+    Transition(
+        transition_id = 'retract',
+        title = _('Retract documet'),
+        source = PUBLISHED,
+        condition = NullCondition,
+        msg = _('Document retracted'),
+        destination = CREATED),
+    )
+
+publishing_workflow = KofaWorkflow(PUBLISHING_TRANSITIONS)
+
+class PublishingWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Document objects to workflow states.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.publishing.state'
+    state_id = 'wf.publishing.id'
+
+
+class PublishingWorkflowInfo(KofaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt CustomerDocument objects to workflow info objects.
+    """
+    grok.context(IPublicDocument)
+    grok.provides(IKofaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = publishing_workflow
+
+
+@grok.subscribe(IPublicDocument, IWorkflowTransitionEvent)
+def handle_public_document_transition_event(obj, event):
+    """Append message to document history and log file.
+
+    """
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    try:
+        grok.getSite().logger.info('%s - %s' % (obj.document_id, msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/fees.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/fees.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/fees.py	(revision 16045)
@@ -0,0 +1,259 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Components for fee management.
+
+Fees here are considered as a pair <PARAMS, VALUES> with both being a
+nested tuple. This is a way to describe a table with nested columns
+and its values.
+
+For example let's have a look at the following table:
+
+  +------+------------------+----------------------+
+  |      | locals           | non-locals           |
+  +------+--------+---------+------------+---------+
+  |      | art    | science | art        | science |
+  +------+--------+---------+------------+---------+
+  |fac1  |    1.1 |     1.2 |        1.3 |     1.4 |
+  +------+--------+---------+------------+---------+
+  |fac2  |    2.1 |     2.2 |        2.3 |     2.4 |
+  +------+--------+---------+------------+---------+
+
+This table contains parameters (all the headings) and numbers. The
+numbers are our VALUES while the headings are our PARAMS.
+
+Now the parameters are organized in a specific way: each value
+represents exactly one heading out of the following sets: {fac1,
+fac2}, {locals, non-locals}, {art, science}.
+
+The value 1.2, for instance, stands for 'fac1' and 'locals' and
+'science' (and it is the only value for this combination of
+parameters).
+
+We can therefore write the set of parameters as a tuple of tuples like
+this::
+
+  (('locals', 'non-locals'), ('art', 'science'), ('fac1', 'fac2'))
+
+This way we define how parameters must be used with an instance of the
+:class:`FeeTable` class below and its methods. Please note that order
+of elements matters in tuples!
+
+We can set whatever headers we like and the order is not important,
+until we want to set/get the single values.
+
+The values then must be set accordingly. If the above tuple defines
+the parameters, then the following nested tuple represents respective
+values that can be assigned to a fixed set of parameters::
+
+  (
+   ((1.1, 2.1),   # locals, art, fac1/fac2
+    (1.2, 2.2)),  # locals, science, fac1/fac2
+   ((1.3, 2.3),   # non-locals, art, fac1/fac2
+    (1.4, 2.4)),  # non-locals, science, fac1/fac2
+  )
+
+This is complicated. Let's have a closer look. If we have N param
+tuples, then the values must be delivered with a nesting level of N as
+well:
+
+  params := (A, B, C)  ->  N=3 -> (((...)))  [tuples with N levels]
+
+Each inner tuple must contain as much elements as the last param. So,
+if C contains three headings, then all inner tuples must be triples:
+
+  params := (A, B, C), C:= ('f1', 'f2', 'f3') -> (((v1, v2, v3), ...))
+
+For practical purposes you can imagine the above values as a table as
+well:
+
+  +-----------+---------+-------+--------+
+  |           |         |  fac1 |   fac2 |
+  +-----------+---------+-------+--------+
+  |locals     | art     |   1.1 |    2.1 |
+  |           | science |   1.2 |    2.2 |
+  |non-locals | art     |   1.3 |    2.3 |
+  |           | science |   1.4 |    2.4 |
+  +-----------+---------+-------+--------+
+
+Now, if you look closely and read the table from left to right
+column-wise and from top to bottom, you will notice that the order of
+appearances of headings is exactly like in the `params` tuple defined
+above:
+
+  ('locals', 'non-locals'), ('art', 'science'), ('fac1', 'fac2')
+
+The only condition is, that column headings are not nested. If they
+are, but row headings are not, then simply 'turn' the table by 90
+degrees and you have a table that can be used as a pattern for input.
+
+This should help to create fee tables out of tuples.
+
+We'll try to provide more convenient input methods in future.
+
+"""
+import grok
+import itertools
+from zope import schema
+from zope.interface import Interface, implementer, Attribute
+from waeup.kofa.utils.helpers import product
+
+class IFees(Interface):
+    """A utility storing fee tables for different academic sessions.
+    """
+    tables = Attribute("""A dict of fee tables with session as key.""")
+
+class IFeeTable(Interface):
+
+    values = Attribute("""Nested tuples containing cell values.""")
+    params = Attribute("""Parameter names required to get"""
+                            """or set the fee table values.""")
+
+    def as_table():
+        """Return fees as a tuple of tuples (of tuples).
+        """
+
+    def as_dict():
+        """Return fees as dict.
+        """
+
+    def import_values(params, values):
+        """Set fees with parameter names and value dictionary.
+        """
+
+    def set_fee(params_tuple, value):
+        """Set a single entry in fee table.
+        """
+
+    def get_fee(params_tuple):
+        """Get single value in fee table denoted by `params_tuple`.
+        """
+
+def nested_list(nested_tuple):
+    """Turn nested tuples in nested lists: ((A,B),(C,)) -> [[A,B],[C,]]
+    """
+    if isinstance(nested_tuple, tuple):
+        return [nested_list(x) for x in nested_tuple]
+    return nested_tuple
+
+def nested_tuple(nested_list):
+    """Turn nested lists in nested tuples: [[A,B],[C,]] -> ((A,B),(C,))
+    """
+    if isinstance(nested_list, list):
+        return tuple([nested_tuple(x) for x in nested_list])
+    return nested_list
+
+@implementer(IFeeTable)
+class FeeTable(grok.Model):
+
+    params = ()
+    values = ()
+
+    def __init__(self, params=(), values=None):
+        if params and values is not None:
+            self.import_values(params=params, values=values)
+        return
+
+    def as_table(self, row_index=None):
+        pass
+
+    def as_dict(self):
+        """Get all fees as dict with param tuples as keys.
+
+        Example:
+
+          {
+            ('locals', 'art'): 1,
+            ('locals', 'science'): 2,
+            ('non-locals', 'art'): 3,
+            ('non-locals', 'science'): 4,
+          }
+        """
+        result = dict()
+        combinations = list(itertools.product(*self.params))
+        for item in combinations:
+            result[item] = self.get_fee(item)
+        return result
+
+    def import_values(self, params=(), values=()):
+        if params and values:
+            self._check(params, values)
+        self.values = values
+        self.params = params
+        return
+
+    def set_fee(self, params_tuple, value):
+        """Set a single entry in fee table.
+
+        The `params_tuple` is described more thoroughly in
+        :meth:`get_fee()`.
+        """
+        values_list = nested_list(self.values)
+        sublist = values_list
+        for num, param in enumerate(params_tuple):
+            col_name_tuple = self.params[num]
+            if param not in col_name_tuple:
+                raise KeyError("%s not in %s" % (param, col_name_tuple))
+            if num == len(self.params) - 1:
+                sublist[col_name_tuple.index(param)] = value
+            else:
+                sublist = sublist[col_name_tuple.index(param)]
+        self.values = nested_tuple(values_list)
+        pass
+
+    def get_fee(self, params_tuple):
+        """Get value in fee table denoted by `param_tuple`.
+
+        The `params_tuple` must contain exactly one value for each
+        tuple in instance `params`. For instance, if we have instance
+        tuples (row and col names)
+
+          (('locals', 'non-locals'), ('art', 'science')),
+
+        then `params_tuple` must be a 2-length tuple with
+        exactly one of `locals`/`non-locals` and exactly one of
+        `art`/`science` in exactly that order (locals/non-locals
+        before art/science).
+        """
+        result = self.values
+        for num, param in enumerate(params_tuple):
+            col_name_tuple = self.params[num]
+            if param not in col_name_tuple:
+                raise KeyError("%s not in %s" % (param, col_name_tuple))
+            result = result[col_name_tuple.index(param)]
+        return result
+
+    def _check(self, params, values):
+        # XXX: something useful here, please
+        return
+
+DEFAULT_FEE_TABLE = FeeTable(
+    params = (('locals', 'non-locals'), ('art', 'science'),
+              ('fac1', 'fac2')),
+    values = (((1.1, 2.1), (1.2, 2.2)), ((1.3, 2.3), (1.4, 2.4)))
+    )
+
+
+@implementer(IFees)
+class Fees(grok.GlobalUtility):
+
+    grok.name('default')
+
+    def __init__(self):
+        self.tables = {
+            '2012': DEFAULT_FEE_TABLE,
+            }
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.html
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.html	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.html	(revision 16045)
@@ -0,0 +1,103 @@
+<h1>Welcome to WAeUP.Kofa
+<br>
+<p class="kofa-content-desc">
+the Information and Registration Portal
+for Schools, Colleges and Universities
+</p>
+</h1>
+
+<p>
+This is the default frontpage written in HTML.
+Kofa's CSS design is based on Twitter's Bootstrap. Visit the
+<a href="http://getbootstrap.com/">Bootstrap documentation</a>
+for further information.
+</p>
+
+<p>
+Kofa is part of the
+<a href="https://waeup.org/">West African eUniversity Project (WAeUP)</a>.
+It is a powerful and comprehensive Student Management System.
+The software is written in Python and built upon the Open Source
+<a href="http://grok.zope.org/">Grok Web Framework</a>.
+</p>
+
+<h2>1. Heading</h2>
+
+<p>Text</p>
+
+<h3>1.2 Heading</h3>
+
+Text
+
+<h2>2. Heading</h2>
+
+<p>Text</p>
+
+<h3>2.1 Heading</h3>
+
+<ol>
+<li>List item</li>
+<li>List item</li>
+<li>List item</li>
+</ol>
+
+<h3>2.2 Heading</h3>
+
+<ul>
+<li>List item</li>
+<li>List item</li>
+<li>List item</li>
+</ul>
+
+
+>>de<<
+<h1>Willkommen auf WAeUP.Kofa,
+<br>
+<small>
+dem Informations- und Registrierungs-Portal für Schulen und Universitäten
+</small>
+</h1>
+
+<p>
+Dies ist die Standard-Startseite des Portals, die in HTML geschrieben wurde.
+Kofas css-Design basiert auf Twitters Bootstrap. Besuchen Sie die
+<a href="http://twitter.github.com/bootstrap/">Bootstrap-Dokumentation</a>
+auf GitHub, um weitere Informationen zu erhalten.
+</p>
+
+<p>
+Kofa ist Teil des
+<a href="https://waeup.org/">West African eUniversity Project (WAeUP)</a>.
+Es ist ein leistungsstarkes and umfassendes Student Management System, welches
+in Python geschrieben wurde und auf dem Open Source Web Framework
+<a href="http://grok.zope.org/">Grok</a>
+basiert.
+</p>
+
+<h2>1. Überschrift</h2>
+
+<p>Text</p>
+
+<h3>1.2 Überschrift</h3>
+
+Text
+
+<h2>2. Überschrift</h2>
+
+<p>Text</p>
+
+<h3>2.1 Überschrift</h3>
+
+<ol>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+</ol>
+
+<h3>2.2 Überschrift</h3>
+
+<ul>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+<li>Listeneintrag</li>
+</ul>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.rst
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.rst	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/frontpage.rst	(revision 16045)
@@ -0,0 +1,69 @@
+Welcome to Kofa
+===============
+
+This is the default frontpage of the portal written
+in `reStructuredText (reST)
+<http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_
+markup language.
+
+Some more reST examples for getting started:
+
+1. Heading
+==========
+
+Text
+
+1.2 Heading
+-----------
+
+Text
+
+2. Heading
+==========
+
+Text
+
+2.1 Heading
+-----------
+
+Text
+
+2.2 Heading
+-----------
+
+Text
+
+>>de<<
+Willkommen in Kofa
+==================
+
+Dies ist die Standard-Startseite des Portals. Sie wurde in der Markup-Sprache
+`reStructuredText (reST) <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_
+geschrieben.
+
+Weitere reST-Beispiele zur Einführung:
+
+1. Überschrift
+==============
+
+Text
+
+1.2 Überschrift
+---------------
+
+Text
+
+2. Überschrift
+==============
+
+Text
+
+2.1 Überschrift
+---------------
+
+Text
+
+2.2 Überschrift
+---------------
+
+Text
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/ftesting.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/ftesting.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/ftesting.zcml	(revision 16045)
@@ -0,0 +1,72 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:kofa="http://namespaces.waeup.org/kofa"
+   i18n_domain="waeup.kofa"
+   package="waeup.kofa"
+   >
+
+  <include package="grok" />
+  <includeOverrides package="waeup.kofa" />
+  <include package="waeup.kofa" file="mail.zcml" />
+  <include package="waeup.kofa" file="async_single.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <kofa:datacenter
+      path="../../../parts/test/datacenter" />
+
+  <!-- Typical functional testing security setup -->
+  <securityPolicy
+      component="zope.securitypolicy.zopepolicy.ZopeSecurityPolicy"
+      />
+
+  <unauthenticatedPrincipal
+      id="zope.anybody"
+      title="Unauthenticated User"
+      />
+  <unauthenticatedGroup id="zope.Anybody"
+      title="Unauthenticated Users" />
+  <authenticatedGroup id="zope.Authenticated"
+      title="Authenticated Users" />
+  <everybodyGroup id="zope.Everybody"
+      title="All Users" />
+
+  <!-- grant
+      permission="zope.View"
+      principal="zope.Anybody"
+      / -->
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Anybody" />
+  <grant
+      permission="zope.View"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="zope.app.dublincore.view"
+      principal="zope.Authenticated"
+      />
+  <grant
+      permission="waeup.Public"
+      principal="zope.Everybody"
+      />
+  <grant 
+      permission="waeup.Anonymous"
+      principal="zope.Anybody"
+      />
+  <grant
+      permission="waeup.Authenticated"
+      principal="zope.Authenticated" />
+
+
+  <principal
+      id="zope.mgr"
+      title="Manager"
+      login="mgr"
+      password="mgrpw"
+      />
+
+  <role id="zope.Manager" title="Site Manager" />
+  <grantAll role="zope.Manager" />
+  <grant role="zope.Manager" principal="zope.mgr" />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/__init__.py	(revision 16045)
@@ -0,0 +1,11 @@
+"""This package contains everything regarding hostels.
+"""
+# Make this a package.
+from waeup.kofa.hostels.container import HostelsContainer
+from waeup.kofa.hostels.hostel import Hostel, Bed
+
+__all__ = [
+    'HostelsContainer',
+    'Hostel',
+    'Bed',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/batching.py	(revision 16045)
@@ -0,0 +1,185 @@
+## $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
+##
+"""Batch processing components for hostels.
+
+"""
+import grok
+from zope.interface import Interface
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import IBatchProcessor, IGNORE_MARKER
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.hostels.interfaces import IHostel, IBed
+from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
+from waeup.kofa.interfaces import MessageFactory as _
+
+class HostelProcessor(BatchProcessor):
+    """The Hostel Procesor imports hostels, i.e. the container objects of
+    beds. It does not import beds. There is nothing special about this
+    processor.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'hostelprocessor'
+    grok.name(util_name)
+
+    name = _('Hostel Processor')
+    iface = IHostel
+
+    location_fields = ['hostel_id',]
+    factory_name = 'waeup.Hostel'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'hostels' in site.keys()
+
+    def entryExists(self, row, site):
+        return row['hostel_id'] in site['hostels'].keys()
+
+    def getParent(self, row, site):
+        return site['hostels']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['hostel_id'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addHostel(obj)
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(HostelProcessor, self).updateEntry(
+            obj, row, site, filename)
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite()['hostels'].logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
+
+
+class BedProcessor(BatchProcessor):
+    """The Bed Procesor update beds. It allocates students
+    to empty beds and switches the reservation status of beds. ``1``
+    means reserved and ``0`` unreserved. Beds cannot be released
+    by import.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'bedupdater'
+    grok.name(util_name)
+
+    name = _('Bed Processor (update only)')
+    iface = IBed
+
+    location_fields = ['hostel_id', 'bed_id']
+    factory_name = None
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return self.location_fields + ['reserved', 'owner']
+
+    def parentsExist(self, row, site):
+        if not 'hostels' in site.keys():
+            return False
+        return row['hostel_id'] in site['hostels']
+
+    def entryExists(self, row, site):
+        if not self.parentsExist(row, site):
+            return False
+        parent = self.getParent(row, site)
+        return row['bed_id'] in parent.keys()
+
+    def getParent(self, row, site):
+        return site['hostels'][row['hostel_id']]
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['bed_id'])
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the bed must fulfill
+        before being updated.
+        """
+        # Check if bed is occupied
+        if row.get('owner') and obj.owner != NOT_OCCUPIED:
+            return 'Bed is occupied.'
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        inv_errs = ''
+        conv_dict = {}
+        errs = []
+        reserved = row.get('reserved')
+        if reserved not in (None, IGNORE_MARKER, '', '0', '1'):
+            errs.append(('reserved','invalid value'))
+        owner = row.get('owner')
+        if owner not in (None, '', IGNORE_MARKER):
+            if owner == NOT_OCCUPIED:
+                errs.append(('owner','bed cannot be released by import'))
+                return errs, inv_errs, conv_dict
+            beds_cat = getUtility(ICatalog, name='beds_catalog')
+            results = list(beds_cat.searchResults(owner=(owner, owner)))
+            if len(results) > 0:
+                errs.append((
+                    'owner','student already resides in %s'
+                    % results[0].bed_id))
+                return errs, inv_errs, conv_dict
+            students_cat = getUtility(ICatalog, name='students_catalog')
+            results = list(students_cat.searchResults(student_id=(owner, owner)))
+            if len(results) != 1:
+                errs.append(('owner','student does not exist'))
+        return errs, inv_errs, conv_dict
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        changed = []
+        owner = row.get('owner')
+        if owner not in (None, '', IGNORE_MARKER):
+            obj.bookBed(owner)
+            changed.append('owner=%s' % owner)
+        reserved = row.get('reserved')
+        sh, sex, bt = obj.bed_type.split('_')
+        if (reserved == '1' and bt != 'reserved') or \
+            (reserved == '0'and bt == 'reserved'):
+            message = obj.switchReservation()
+            changed.append(message)
+        # Log actions...
+        if changed:
+            items_changed = ', '.join(changed)
+        else:
+            items_changed = 'nothing'
+        location_field = self.location_fields[1]
+        grok.getSite()['hostels'].logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser.py	(revision 16045)
@@ -0,0 +1,463 @@
+## $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 hostels and related components.
+"""
+import grok
+import sys
+from zope.i18n import translate
+from zope.component import getUtility, createObject
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.browser.layout import (
+    KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    NullValidator, UtilityView)
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.layout import default_primary_nav_template
+from waeup.kofa.browser.pages import delSubobjects
+from waeup.kofa.browser.viewlets import (
+    ManageActionButton, PrimaryNavTab)
+from waeup.kofa.browser.layout import jsaction, action
+from waeup.kofa.interfaces import IKofaObject, IKofaUtils, DOCLINK
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
+from waeup.kofa.hostels.hostel import Hostel
+from waeup.kofa.hostels.interfaces import (
+    IHostelsContainer, IHostel, IBed)
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+
+# Save function used for save methods in manager 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.context._p_changed = True
+    view.flash(_('Form has been saved.'))
+    if fields_string:
+        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
+    return
+
+class HostelsTab(PrimaryNavTab):
+    """Hostels tab in primary navigation.
+    """
+
+    grok.context(IKofaObject)
+    grok.order(5)
+    grok.require('waeup.viewHostels')
+    grok.name('hostelstab')
+    template = default_primary_nav_template
+    pnav = 5
+    tab_title = _(u'Hostels')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('hostels')
+
+class HostelsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the hostels container.
+    """
+    grok.context(IHostelsContainer)
+    title = _(u'Hostels')
+
+class HostelBreadcrumb(Breadcrumb):
+    """A breadcrumb for the hostel container.
+    """
+    grok.context(IHostel)
+
+    def title(self):
+        return self.context.hostel_name
+
+class BedBreadcrumb(Breadcrumb):
+    """A breadcrumb for the hostel container.
+    """
+    grok.context(IBed)
+
+    def title(self):
+        co = self.context.coordinates
+        return _('Block ${a}, Room ${b}, Bed ${c}',
+            mapping = {'a':co[1], 'b':co[2], 'c':co[3]})
+
+class HostelsContainerPage(KofaDisplayFormPage):
+    """The standard view for hostels containers.
+    """
+    grok.context(IHostelsContainer)
+    grok.name('index')
+    grok.require('waeup.viewHostels')
+    grok.template('containerpage')
+    label = _('Accommodation Section')
+    pnav = 5
+    form_fields = grok.AutoFields(IHostelsContainer)
+    form_fields[
+        'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+
+class HostelsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IHostelsContainer)
+    grok.view(HostelsContainerPage)
+    grok.require('waeup.manageHostels')
+    text = _('Manage accommodation section')
+
+class HostelsStatisticsActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IHostelsContainer)
+    grok.view(HostelsContainerPage)
+    grok.require('waeup.manageHostels')
+    icon = 'actionicon_statistics.png'
+    text = _('Bed statistics')
+    target = 'statistics'
+
+class ReleaseExpiredAllocationsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IHostelsContainer)
+    grok.view(HostelsContainerPage)
+    grok.require('waeup.manageHostels')
+    icon = 'actionicon_sweep.png'
+    target = 'releaseexpired'
+    text = _('Release all expired bed space allocations')
+
+    @property
+    def target_url(self):
+        if self.target and self.context.allocation_expiration:
+            return self.view.url(self.view.context, self.target)
+        return
+
+    @property
+    def onclick(self):
+        msg = _("'All expired bed space allocations will be annulled. " + \
+              "Are you sure? \\n\\n" + \
+              "Bed space allocation expires ${a} days after accommodation " + \
+              "booking if maintenance fee is not " + \
+              "paid.'")
+        msg = _(msg, mapping={'a': self.context.allocation_expiration,})
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return "return window.confirm(%s);" % translate(
+            msg, 'waeup.kofa', target_language=portal_language)
+
+class HostelsContainerManagePage(KofaEditFormPage):
+    """The manage page for hostel containers.
+    """
+    grok.context(IHostelsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageHostels')
+    grok.template('containermanagepage')
+    pnav = 5
+    label = _('Manage accommodation section')
+    form_fields = grok.AutoFields(IHostelsContainer)
+    taboneactions = [_('Save')]
+    tabtwoactions = [_('Add hostel'),
+        _('Clear all hostels'),
+        _('Remove selected')]
+    doclink = DOCLINK + '/hostels.html#accommodation-section'
+
+    # It's quite dangerous to remove entire hostels with its content (beds).
+    # Thus, this remove method should be combined with an archiving function.
+    @jsaction(_('Remove selected'))
+    def delHostels(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            deleted = []
+            child_id = form['val_id']
+            if not isinstance(child_id, list):
+                child_id = [child_id]
+            for id in child_id:
+                deleted.append(id)
+            self.context.writeLogMessage(
+                self, 'deleted: % s' % ', '.join(deleted))
+        delSubobjects(self, redirect='@@manage', tab='2')
+        return
+
+    @action(_('Add hostel'), validator=NullValidator)
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, 'addhostel'))
+        return
+
+    @jsaction(_('Clear all hostels'), style='danger')
+    def clearHostels(self, **data):
+        self.context.clearAllHostels()
+        self.flash(_('All hostels cleared.'))
+        self.context.writeLogMessage(self, 'all hostels cleared')
+        self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        if data['accommodation_session'] != self.context.accommodation_session:
+            catalog = getUtility(ICatalog, name='beds_catalog')
+            beds = catalog.searchResults(bed_type=(None,None))
+            if len(beds) and not self.request.principal.id == 'admin':
+                self.flash(_('You can\'t change the booking session '
+                             'before clearing all hostels.'),
+                           type='warning')
+                return
+        msave(self, **data)
+        return
+
+class ReleaseExpiredAllocationsPage(UtilityView, grok.View):
+    """Release all expired allocated beds.
+    """
+    grok.context(IHostelsContainer)
+    grok.name('releaseexpired')
+    grok.require('waeup.manageHostels')
+
+    def update(self):
+        n = self.context.allocation_expiration
+        if not n:
+            self.flash(
+                _('Forbidden'), type='danger')
+            self.redirect(self.url(self.context))
+            return
+        if not grok.getSite()['configuration'].maintmode_enabled_by:
+            self.flash(
+                _('Portal must be in maintenance mode for releasing expired '
+                  'bed allocations.'),
+                type='danger')
+            self.redirect(self.url(self.context))
+            return
+        released = self.context.releaseExpiredAllocations(n)
+        if len(released):
+            message = ', '.join(released)
+            self.context.writeLogMessage(self, 'released: %s' % message)
+            if len(released) > 50:
+                flash_msg = _('Successfully released ${a} beds.',
+                    mapping = {'a':len(released)})
+            else:
+                flash_msg = _('Successfully released beds: ${a}',
+                    mapping = {'a':message})
+        else:
+            flash_msg = _('No bed released.')
+        self.flash(flash_msg, type='success')
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class HostelsStatisticsPage(KofaDisplayFormPage):
+    """Some statistics about beds in hostels.
+    """
+    grok.context(IHostelsContainer)
+    grok.name('statistics')
+    grok.require('waeup.manageHostels')
+    grok.template('containerstatistics')
+    label = _('Bed Statistics')
+
+class HostelAddFormPage(KofaAddFormPage):
+    """Add-form to add a hostel.
+    """
+    grok.context(IHostelsContainer)
+    grok.require('waeup.manageHostels')
+    grok.name('addhostel')
+    #grok.template('hosteladdpage')
+    form_fields = grok.AutoFields(IHostel).omit('beds_reserved', 'hostel_id')
+    label = _('Add hostel')
+    pnav = 5
+    doclink = DOCLINK + '/hostels.html#accommodation-section'
+
+    @action(_('Create hostel'))
+    def addHostel(self, **data):
+        hostel = container = createObject(u'waeup.Hostel')
+        self.applyData(hostel, **data)
+        hostel.hostel_id = data['hostel_name'].lower().replace(
+            ' ','-').replace('_','-').replace('.','')
+        try:
+            self.context.addHostel(hostel)
+        except KeyError:
+            self.flash(_('The hostel already exists.'), type='warning')
+            return
+        self.flash(_('Hostel created.'))
+        self.context.writeLogMessage(self, 'added: % s' % data['hostel_name'])
+        self.redirect(self.url(self.context[hostel.hostel_id], 'index'))
+        return
+
+class HostelDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display hostel data
+    """
+    grok.context(IHostel)
+    grok.name('index')
+    grok.require('waeup.viewHostels')
+    grok.template('hostelpage')
+    form_fields = grok.AutoFields(IHostel).omit('beds_reserved')
+    pnav = 5
+
+    @property
+    def label(self):
+        return self.context.hostel_name
+
+class HostelManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IHostel)
+    grok.view(HostelDisplayFormPage)
+    grok.require('waeup.manageHostels')
+    text = _('Manage')
+    target = 'manage'
+
+class HostelManageFormPage(KofaEditFormPage):
+    """ View to edit hostel data
+    """
+    grok.context(IHostel)
+    grok.name('manage')
+    grok.require('waeup.manageHostels')
+    form_fields = grok.AutoFields(IHostel).omit('hostel_id', 'beds_reserved')
+    grok.template('hostelmanagepage')
+    label = _('Manage hostel')
+    pnav = 5
+    taboneactions = [_('Save')]
+    tabtwoactions = [_('Update all beds'),
+        _('Switch reservation of selected beds'),
+        _('Release selected beds'),
+        _('Clear hostel')]
+    not_occupied = NOT_OCCUPIED
+    doclink = DOCLINK + '/hostels.html#browser-pages'
+
+    @property
+    def students_url(self):
+        return self.url(grok.getSite(),'students')
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Update all beds'), style='primary',
+            warning=_('Attention: The updater removes all reservation flags of existing beds.'
+                        ' You really want to update?'))
+    def updateBeds(self, **data):
+        if not grok.getSite()['configuration'].maintmode_enabled_by:
+            self.flash(
+                _('Portal must be in maintenance mode for bed updates.'),
+                type='danger')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        removed, added, modified, modified_beds = self.context.updateBeds()
+        message = '%d empty beds removed, %d beds added, %d occupied beds modified (%s)' % (
+            removed, added, modified, modified_beds)
+        if modified > 50:
+            flash_message = _(
+                '${a} empty beds removed, ${b} beds added, '
+                + '${c} occupied beds modified',
+                mapping = {'a':removed, 'b':added, 'c':modified})
+        else:
+            flash_message = _(
+                '${a} empty beds removed, ${b} beds added, '
+                + '${c} occupied beds modified (${d})',
+                mapping = {'a':removed, 'b':added, 'c':modified, 'd':modified_beds})
+        self.flash(flash_message)
+        self.context.writeLogMessage(self, message)
+        self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+    @action(_('Switch reservation of selected beds'))
+    def switchReservations(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No item selected.'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        switched = [] # for log file
+        switched_translated = [] # for flash message
+        # Here we know that the cookie has been set
+        preferred_language = self.request.cookies.get('kofa.language')
+        for bed_id in child_id:
+            message = self.context[bed_id].switchReservation()
+            switched.append('%s (%s)' % (bed_id,message))
+            m_translated = translate(message, 'waeup.kofa',
+                target_language=preferred_language)
+            switched_translated.append('%s (%s)' % (bed_id,m_translated))
+        if len(switched):
+            message = ', '.join(switched)
+            m_translated = ', '.join(switched_translated)
+            if len(switched) > 50:
+                self.flash(_('Successfully switched ${a} beds.',
+                    mapping = {'a':len(switched)}))
+            else:
+                self.flash(_('Successfully switched beds: ${a}',
+                    mapping = {'a':m_translated}))
+            self.context.writeLogMessage(self, 'switched: %s' % message)
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+    @action(_('Release selected beds'))
+    def releaseBeds(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No item selected.'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        released = []
+        for bed_id in child_id:
+            message = self.context[bed_id].releaseBed()
+            if message:
+                released.append('%s (%s)' % (bed_id,message))
+        if len(released):
+            message = ', '.join(released)
+            self.flash(_('Successfully released beds: ${a}',
+                mapping = {'a':message}))
+            self.context.writeLogMessage(self, 'released: %s' % message)
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        else:
+            self.flash(_('No allocated bed selected.'), type='warning')
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+    @jsaction(_('Clear hostel'), style='danger')
+    def clearHostel(self, **data):
+        self.context.clearHostel()
+        self.flash(_('Hostel cleared.'))
+        self.context.writeLogMessage(self, 'cleared')
+        self.redirect(self.url(self.context, '@@manage')+'#tab2')
+        return
+
+class BedManageFormPage(KofaEditFormPage):
+    """ View to edit bed data
+    """
+    grok.context(IBed)
+    grok.name('index')
+    grok.require('waeup.manageHostels')
+    form_fields = grok.AutoFields(IBed).omit(
+        'bed_id', 'bed_number', 'bed_type')
+    label = _('Allocate student')
+    pnav = 5
+    doclink = DOCLINK + '/hostels.html#manage-bed'
+
+    @action(_('Save'))
+    def save(self, **data):
+        if data['owner'] == NOT_OCCUPIED:
+            self.flash(_('No valid student id.'), type='warning')
+            self.redirect(self.url(self.context))
+            return
+        msave(self, **data)
+        self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
+        return
+
+    def update(self):
+        if self.context.owner != NOT_OCCUPIED:
+            # Don't use this form for exchanging students.
+            # Beds must be released first before they can be allocated to
+            # other students.
+            self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt	(revision 16045)
@@ -0,0 +1,70 @@
+<form action="." tal:attributes="action request/URL"
+    i18n:domain="waeup.kofa"
+    method="POST" enctype="multipart/form-data">
+
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab" i18n:translate="">
+        Settings
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab" i18n:translate="">
+        Hostels
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+            tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Id
+            </th>
+            <th i18n:translate="">Sort Id
+            </th>
+            <th i18n:translate="">Name
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="hostel context/values">
+            <td>
+              <input type="checkbox" name="val_id" tal:attributes="value hostel/hostel_id" />
+           </td>
+           <td>
+              <a tal:attributes="href python: view.url(hostel)" tal:content="hostel/__name__"> ID</a>
+           </td>
+           <td tal:content="hostel/sort_id">SORT_ID</td>
+           <td><span tal:content="hostel/hostel_name">ID</span></td>
+          </tr>
+        </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+            tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerpage.pt	(revision 16045)
@@ -0,0 +1,40 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <span tal:content="widget/label">LABEL</span>:
+        </td>
+        <td>
+          <span tal:content="structure widget">
+            <input type="text" />
+          </span>
+        </td>
+      </tr>
+    </tal:block>
+  </tbody>
+</table>
+
+<table i18n:domain="waeup.kofa" class="table">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Sort Id</th>
+      <th i18n:translate="">Name</th>
+      <th i18n:translate="">Booked Beds</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tal:tbody repeat="value context/values">
+      <tr tal:define = "stats value/bed_statistics">
+        <td> <a tal:attributes="href python: view.url(value)">
+            <span tal:content="value/hostel_id">ID</span></a></td>
+        <td tal:content="value/sort_id">SORT_ID</td>
+        <td tal:content="value/hostel_name">NAME</td>
+        <td>
+          <span tal:replace="stats/booked">10</span> of <span tal:replace="stats/total">100</span>
+        </td>
+      </tr>
+    </tal:tbody>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerstatistics.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerstatistics.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/containerstatistics.pt	(revision 16045)
@@ -0,0 +1,20 @@
+<tal:statistics define="stats context/statistics" i18n:domain="waeup.kofa">
+  <table class="table">
+    <thead>
+      <tr>
+        <th i18n:translate="" width="40%">Bed Type</th>
+        <th i18n:translate="">Occupied</th>
+        <th i18n:translate="">Free</th>
+        <th i18n:translate="">Total</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="bed_type python:sorted(stats.keys())">
+        <td tal:content="bed_type">TYPE</td>
+        <td style="width: 100px;" tal:content="python: stats[bed_type][0]">OCCUPIED</td>
+        <td style="width: 100px;" tal:content="python: stats[bed_type][1]">FREE</td>
+        <td style="width: 100px;" tal:content="python: stats[bed_type][2]">TOTAL</td>
+      </tr>
+    </tbody>
+  </table>
+</tal:statistics>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt	(revision 16045)
@@ -0,0 +1,87 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Hostel Data</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Beds</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+      <tbody>
+        <tal:widgets content="structure provider:widgets" />
+      </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th>
+            <th i18n:translate="">Id</th>
+            <th i18n:translate="">Type</th>
+            <th i18n:translate="">Number</th>
+            <th i18n:translate="">Owner</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="value context/values">
+             <td>
+              <input type="checkbox" name="val_id"
+        		  tal:attributes="value value/__name__" />
+            </td>
+            <td tal:content="value/bed_id">ID</td>
+            <td tal:content="value/bed_type">TYPE</td>
+            <td tal:content="value/bed_number">NUMBER</td>
+            <td>
+              <a tal:condition="python: value.owner != view.not_occupied"
+                 tal:attributes="href python: '%s/%s/accommodation' %
+                      (view.students_url,value.owner)">
+                <span tal:content="value/owner">OWNER</span>
+              </a>
+              <a tal:condition="python: value.owner == view.not_occupied"
+                 tal:attributes="href python: view.url(value)"
+                 i18n:translate="">
+              [allocate student]
+              </a>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <input type="checkbox" onClick="toggle(this, 'val_id')" />
+      &nbsp;<span  i18n:translate="">Select all</span>
+      <br /><br />
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+      <br />
+
+    </div>
+  </div>
+</form>  
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/browser_templates/hostelpage.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/catalog.py	(revision 16045)
@@ -0,0 +1,34 @@
+## $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
+##
+"""Cataloging and searching components for hostels.
+"""
+import grok
+from grok import index
+from waeup.kofa.interfaces import IUniversity
+from waeup.kofa.hostels.interfaces import IBed
+
+class BedIndexes(grok.Indexes):
+    """A catalog for beds.
+    """
+    grok.site(IUniversity)
+    grok.name('beds_catalog')
+    grok.context(IBed)
+
+    bed_type = index.Field(attribute='bed_type')
+    owner = index.Field(attribute='owner')
+    bed_id = index.Field(attribute='bed_id')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/container.py	(revision 16045)
@@ -0,0 +1,100 @@
+## $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
+##
+"""
+Containers which contain hostels.
+"""
+import grok
+import pytz
+from datetime import datetime
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, ComponentLookupError
+from waeup.kofa.hostels.interfaces import (
+    IHostelsContainer, IHostel, IHostelsUtils)
+from waeup.kofa.utils.logger import Logger
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class HostelsContainer(grok.Container, Logger):
+    """This is a container for all kind of hostels.
+    """
+    grok.implements(IHostelsContainer)
+    grok.provides(IHostelsContainer)
+
+    def __init__(self):
+        super(HostelsContainer, self).__init__()
+        return
+
+    def addHostel(self, hostel):
+        """Add a hostel.
+        """
+        if not IHostel.providedBy(hostel):
+            raise TypeError(
+                'HostelsContainers contain only IHostel instances')
+        self[hostel.hostel_id] = hostel
+        return
+
+    def clearAllHostels(self):
+        """Clear all hostels.
+        """
+        for hostel in self.values():
+            hostel.clearHostel()
+        return
+
+    def releaseExpiredAllocations(self, n=7):
+        """Release bed if bed allocation has expired. Allocation expires
+        after `n` days if maintenance fee has not been paid.
+        """
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(owner=(None, None))
+        released = []
+        for bed in results:
+            student_id = bed.releaseBedIfMaintenanceNotPaid(n=n)
+            if student_id:
+                released.append('%s (%s)' % (bed.bed_id,student_id))
+        return released
+
+    @property
+    def expired(self):
+        # Check if application has started ...
+        if not self.startdate or (
+            self.startdate > datetime.now(pytz.utc)):
+            return True
+        # ... or ended
+        if not self.enddate or (
+            self.enddate < datetime.now(pytz.utc)):
+            return True
+        return False
+
+    @property
+    def statistics(self):
+        try:
+          statistics = getUtility(
+              IHostelsUtils).getBedStatistics()
+        except ComponentLookupError:  # happens in unit tests
+            return
+        return statistics
+
+    logger_name = 'waeup.kofa.${sitename}.hostels'
+    logger_filename = 'hostels.log'
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+HostelsContainer = attrs_to_fields(HostelsContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/export.py	(revision 16045)
@@ -0,0 +1,70 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for hostels and beds.
+"""
+import grok
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names
+from waeup.kofa.hostels.interfaces import IBed, IHostel
+
+class HostelExporter(grok.GlobalUtility, ExporterBase):
+    """The Hostel Exporter exports container data. It does not
+    export beds inside the container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('hostels')
+
+    fields = tuple(sorted(iface_names(IHostel))) 
+    title = _(u'Hostels')
+
+    def export_all(self, site, filepath=None):
+        """Export hostels into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        hostels = site.get('hostels', {})
+        for hostel in hostels.values():
+            self.write_item(hostel, writer)
+        return self.close_outfile(filepath, outfile)
+
+
+class BedExporter(grok.GlobalUtility, ExporterBase):
+    """The Bed Exporter exports all beds stored in the
+    hostel containers. The exporter iterates over all hostels
+    and over all beds inside each hostel container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('beds')
+
+    fields = tuple(sorted(iface_names(IBed))) + (
+        'hall', 'block', 'room', 'bed', 'special_handling', 'sex', 'bt')
+    title = _(u'Beds')
+
+    def export_all(self, site, filepath=None):
+        """Export beds into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        hostels = site.get('hostels', {})
+        for hostel in hostels.values():
+            for bed in hostel.values():
+                self.write_item(bed, writer)
+        return self.close_outfile(filepath, outfile)
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/hostel.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/hostel.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/hostel.py	(revision 16045)
@@ -0,0 +1,397 @@
+## $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
+##
+"""
+These are the hostels.
+"""
+import grok
+from zope.event import notify
+from zope.component import getUtility, createObject
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.catalog.interfaces import ICatalog
+from zope.catalog.field import FieldIndex
+from datetime import datetime
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
+from waeup.kofa.hostels.interfaces import IHostel, IBed
+from waeup.kofa.students.interfaces import IBedTicket
+from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import now, reindex_cat
+
+class Hostel(grok.Container):
+    """This is a hostel.
+    """
+    grok.implements(IHostel)
+    grok.provides(IHostel)
+
+    @property
+    def bed_statistics(self):
+        total = len(self.keys())
+        booked = 0
+        for value in self.values():
+            if value.owner != NOT_OCCUPIED:
+                booked += 1
+        return {'booked':booked, 'total':total}
+
+    def clearHostel(self):
+        """Remove all beds
+        """
+        keys = [i for i in self.keys()] # create deep copy
+        for bed in keys:
+            del self[bed]
+        return
+
+    def addBed(self, bed):
+        """Add a bed.
+        """
+        if not IBed.providedBy(bed):
+            raise TypeError(
+                'Hostels contain only IBed instances')
+        self[bed.bed_id] = bed
+        return
+
+    def updateBeds(self):
+        """Fill hostel with beds or update beds.
+        """
+        added_counter = 0
+        modified_counter = 0
+        removed_counter = 0
+        modified_beds = u''
+
+        # Remove all empty beds. Occupied beds remain in hostel!
+        keys = list(self.keys()) # create list copy
+        for key in keys:
+            if self[key].owner == NOT_OCCUPIED:
+                del self[key]
+                self._p_changed = True
+                removed_counter += 1
+            else:
+                self[key].bed_number = 9999
+        remaining = len(keys) - removed_counter
+
+        blocks_for_female = getattr(self,'blocks_for_female',[])
+        blocks_for_male = getattr(self,'blocks_for_male',[])
+        beds_for_fresh = getattr(self,'beds_for_fresh',[])
+        beds_for_pre = getattr(self,'beds_for_pre',[])
+        beds_for_returning = getattr(self,'beds_for_returning',[])
+        beds_for_final = getattr(self,'beds_for_final',[])
+        beds_for_all = getattr(self,'beds_for_all',[])
+        all_blocks = blocks_for_female + blocks_for_male
+        all_beds = (beds_for_pre + beds_for_fresh +
+            beds_for_returning + beds_for_final + beds_for_all)
+        floor_base = 100
+        if self.rooms_per_floor > 99:
+            floor_base = 1000
+        for block in all_blocks:
+            sex = 'male'
+            if block in blocks_for_female:
+                sex = 'female'
+            for floor in range(1,int(self.floors_per_block)+1):
+                for room in range(1,int(self.rooms_per_floor)+1):
+                    for bed in all_beds:
+                        room_nr = floor*floor_base + room
+                        bt = 'all'
+                        if bed in beds_for_fresh:
+                            bt = 'fr'
+                        elif bed in beds_for_pre:
+                            bt = 'pr'
+                        elif bed in beds_for_final:
+                            bt = 'fi'
+                        elif bed in beds_for_returning:
+                            bt = 're'
+                        bt = u'%s_%s_%s' % (self.special_handling,sex,bt)
+                        uid = u'%s_%s_%d_%s' % (
+                            self.hostel_id,block,room_nr,bed)
+                        if uid in self:
+                            bed = self[uid]
+                            # Renumber remaining bed
+                            bed.bed_number = len(self) + 1 - remaining
+                            remaining -= 1
+                            if bed.bed_type != bt:
+                                bed.bed_type = bt
+                                modified_counter += 1
+                                modified_beds += '%s, ' % uid
+                                notify(grok.ObjectModifiedEvent(bed))
+                        else:
+                            bed = createObject(u'waeup.Bed')
+                            bed.bed_id = uid
+                            bed.bed_type = bt
+                            bed.bed_number = len(self) + 1 - remaining
+                            bed.owner = NOT_OCCUPIED
+                            self.addBed(bed)
+                            added_counter +=1
+        return removed_counter, added_counter, modified_counter, modified_beds
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+Hostel = attrs_to_fields(Hostel)
+
+class Bed(grok.Container):
+    """This is a bed.
+    """
+    grok.implements(IBed)
+    grok.provides(IBed)
+
+    @property
+    def coordinates(self):
+        """Determine the coordinates from the bed_id.
+        """
+        return self.bed_id.split('_')
+
+    # The following property attributes are only needed
+    # for the exporter to ease evaluation with Excel.
+
+    @property
+    def hall(self):
+        return self.coordinates[0]
+
+    @property
+    def block(self):
+        return self.coordinates[1]
+
+    @property
+    def room(self):
+        return self.coordinates[2]
+
+    @property
+    def bed(self):
+        return self.coordinates[3]
+
+    @property
+    def special_handling(self):
+        return self.bed_type.split('_')[0]
+
+    @property
+    def sex(self):
+        return self.bed_type.split('_')[1]
+
+    @property
+    def bt(self):
+        return self.bed_type.split('_')[2]
+
+
+    def bookBed(self, student_id):
+        if self.owner == NOT_OCCUPIED:
+            self.owner = student_id
+            notify(grok.ObjectModifiedEvent(self))
+            return None
+        else:
+            return self.owner
+
+    def switchReservation(self):
+        """Reserves or unreserve bed respectively.
+        """
+        sh, sex, bt = self.bed_type.split('_')
+        hostel_id, block, room_nr, bed = self.coordinates
+        hostel = self.__parent__
+        beds_for_fresh = getattr(hostel,'beds_for_fresh',[])
+        beds_for_pre = getattr(hostel,'beds_for_pre',[])
+        beds_for_returning = getattr(hostel,'beds_for_returning',[])
+        beds_for_final = getattr(hostel,'beds_for_final',[])
+        bed_string = u'%s_%s_%s' % (block, room_nr, bed)
+        if bt == 'reserved':
+            bt = 'all'
+            if bed in beds_for_fresh:
+                bt = 'fr'
+            elif bed in beds_for_pre:
+                bt = 'pr'
+            elif bed in beds_for_final:
+                bt = 'fi'
+            elif bed in beds_for_returning:
+                bt = 're'
+            bt = u'%s_%s_%s' % (sh, sex, bt)
+            message = _(u'unreserved')
+        else:
+            bt = u'%s_%s_reserved' % (sh, sex)
+            message = _(u'reserved')
+        self.bed_type = bt
+        notify(grok.ObjectModifiedEvent(self))
+        return message
+
+    def releaseBed(self):
+        """Release bed.
+        """
+        if self.owner == NOT_OCCUPIED:
+            return
+        old_owner = self.owner
+        self.owner = NOT_OCCUPIED
+        notify(grok.ObjectModifiedEvent(self))
+        accommodation_session = grok.getSite()[
+            'hostels'].accommodation_session
+        try:
+            bedticket = grok.getSite()['students'][old_owner][
+                          'accommodation'][str(accommodation_session)]
+        except KeyError:
+            return '%s without bed ticket' % old_owner
+        bedticket.bed = None
+        tz = getUtility(IKofaUtils).tzinfo
+        timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        bedticket.bed_coordinates = u'-- booking cancelled on %s --' % (
+            timestamp,)
+        return old_owner
+
+    def releaseBedIfMaintenanceNotPaid(self, n=7):
+        """Release bed if maintenance fee has not been paid on time or
+        if bed ticket does not exist. Reserve bed so that it cannot be
+        automatically booked by someone else.
+        """
+        if self.owner == NOT_OCCUPIED:
+            return
+        accommodation_session = grok.getSite()[
+            'hostels'].accommodation_session
+        try:
+            bedticket = grok.getSite()['students'][self.owner][
+                          'accommodation'][str(accommodation_session)]
+        except KeyError:
+            old_owner = self.owner
+            self.owner = NOT_OCCUPIED
+            sh, sex, bt = self.bed_type.split('_')
+            bt = u'%s_%s_reserved' % (sh, sex)
+            self.bed_type = bt
+            notify(grok.ObjectModifiedEvent(self))
+            ##: Add logging message
+            return "%s wob" % old_owner # owner without bed ticket
+        if bedticket.maint_payment_made:
+            return
+        jetzt = datetime.utcnow()
+        days_ago = getattr(jetzt - bedticket.booking_date, 'days')
+        if days_ago >= n:
+            old_owner = self.owner
+            self.owner = NOT_OCCUPIED
+            sh, sex, bt = self.bed_type.split('_')
+            bt = u'%s_%s_reserved' % (sh, sex)
+            self.bed_type = bt
+            notify(grok.ObjectModifiedEvent(self))
+            bedticket.bed = None
+            tz = getUtility(IKofaUtils).tzinfo
+            timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+            bedticket.bed_coordinates = u'-- booking expired (%s) --' % (
+                timestamp,)
+            return old_owner
+        return
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+Bed = attrs_to_fields(Bed)
+
+class HostelFactory(grok.GlobalUtility):
+    """A factory for hostels.
+
+    We need this factory for the hostel processor.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Hostel')
+    title = u"Create a new hostel.",
+    description = u"This factory instantiates new hostel instances."
+
+    def __call__(self, *args, **kw):
+        return Hostel()
+
+    def getInterfaces(self):
+        return implementedBy(Hostel)
+
+class BedFactory(grok.GlobalUtility):
+    """A factory for  beds.
+
+    We need this factory to ease customization.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Bed')
+    title = u"Create a new bed.",
+    description = u"This factory instantiates new bed instances."
+
+    def __call__(self, *args, **kw):
+        return Bed()
+
+    def getInterfaces(self):
+        return implementedBy(Bed)
+
+class HostelsPlugin(grok.GlobalUtility):
+    """A plugin to update beds_catalog
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('hostels')
+    log_prefix = 'HostelsPlugin'
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        site_name = getattr(site, '__name__', '<Unnamed Site>')
+        nothing_to_do = True
+        # Add bed_id index
+        try:
+            cat = getUtility(ICatalog, name='beds_catalog')
+            if 'bed_id' not in cat.keys():
+                nothing_to_do = False
+                # replace original `updateIndex` method
+                def updateIndexReplacement(index):
+                    reindex_cat(cat)
+                cat._updateIndex = cat.updateIndex
+                cat.updateIndex = updateIndexReplacement
+                # setup catalog
+                cat[u'bed_id'] = FieldIndex(field_name=u'bed_id')
+                cat.updateIndex = cat._updateIndex  # undo changes
+                logger.info(
+                    '%s: bed_id index added to beds_catalog.'
+                    % self.log_prefix)
+                reindex_cat(cat)
+                logger.info(
+                    '%s: beds_catalog updated.'
+                    % self.log_prefix)
+        except ComponentLookupError: # in unit tests
+            pass
+        if nothing_to_do:
+            logger.info(
+                '%s: Updating site at %s: Nothing to do.' % (
+                    self.log_prefix, site_name,)
+                )
+        return
+
+
+@grok.subscribe(IBedTicket, grok.IObjectRemovedEvent)
+def handle_bedticket_removed(bedticket, event):
+    """If a bed ticket is deleted, we make sure that also the owner attribute
+    of the bed is cleared (set to NOT_OCCUPIED).
+    """
+    if bedticket.bed != None:
+        bedticket.bed.owner = NOT_OCCUPIED
+        notify(grok.ObjectModifiedEvent(bedticket.bed))
+
+@grok.subscribe(IBed, grok.IObjectRemovedEvent)
+def handle_bed_removed(bed, event):
+    """If a bed is deleted, we make sure that the bed object is
+    removed also from the owner's bed ticket.
+    """
+    if bed.owner == NOT_OCCUPIED:
+        return
+    accommodation_session = grok.getSite()['hostels'].accommodation_session
+    try:
+        bedticket = grok.getSite()['students'][bed.owner][
+                      'accommodation'][str(accommodation_session)]
+    except KeyError:
+        return
+    bedticket.bed = None
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/interfaces.py	(revision 16045)
@@ -0,0 +1,313 @@
+## $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
+##
+from  grok import getSite
+from datetime import datetime
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.interface import invariant, Invalid, Attribute, Interface
+from zope import schema
+from waeup.kofa.interfaces import (
+    IKofaObject, academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.hostels.vocabularies import (
+    bed_letters, blocks, SpecialHandlingSource,
+    NOT_OCCUPIED)
+
+# Define a validation method for sort ids
+class NotASortId(schema.ValidationError):
+    __doc__ = u"Invalid sort_id"
+
+def validate_sort_id(value):
+    if not value < 1000:
+        raise NotASortId(value)
+    return True
+
+class IHostelsContainer(IKofaObject):
+    """A container for hostel objects.
+    """
+
+    expired = Attribute('True if current datetime is in application period.')
+    statistics = Attribute('Bed category statistics')
+
+    startdate = schema.Datetime(
+        title = _(u'Hostel Allocation Start Date'),
+        required = False,
+        description = _('Example: ') + u'2011-12-01 18:30:00+01:00',
+        )
+
+    enddate = schema.Datetime(
+        title = _(u'Hostel Allocation Closing Date'),
+        required = False,
+        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
+        )
+
+    accommodation_session = schema.Choice(
+        title = _(u'Booking Session'),
+        source = academic_sessions_vocab,
+        #default = datetime.now().year,
+        required = False,
+        readonly = False,
+        )
+
+    accommodation_states = schema.List(
+        title = _(u'Allowed States'),
+        value_type = schema.Choice(
+            vocabulary = registration_states_vocab,
+            ),
+        defaultFactory=list,
+        )
+
+    allocation_expiration = schema.Int(
+        title = _(u'Allocation Expiration Time (days)'),
+        description = _(
+            'Number of days after which allocation is being annulled'),
+        required = False,
+        )
+
+    def clearAllHostels():
+        """Clear all hostels.
+        """
+
+    def addHostel(hostel):
+        """Add a hostel.
+        """
+
+    def releaseExpiredAllocations(n):
+        """Release bed if bed allocation has expired. Allocation expires
+        after `n` days if maintenance fee has not been paid.
+        """
+
+    def writeLogMessage(view, message):
+        """Add an INFO message to hostels.log.
+        """
+
+class IHostel(IKofaObject):
+    """Representation of a hostel.
+    """
+
+    bed_statistics = Attribute('Number of booked and total beds')
+
+    def clearHostel():
+        """Remove all beds.
+        """
+
+    def updateBeds():
+        """Fill hostel with beds or update beds.
+        """
+
+    hostel_id = schema.TextLine(
+        title = _(u'Hostel Id'),
+        )
+
+    sort_id = schema.Int(
+        title = _(u'Sort Id'),
+        required = True,
+        default = 10,
+        constraint=validate_sort_id,
+        )
+
+    hostel_name = schema.TextLine(
+        title = _(u'Hostel Name'),
+        required = True,
+        default = u'Hall 1',
+        )
+
+    floors_per_block = schema.Int(
+        title = _(u'Floors per Block'),
+        required = True,
+        default = 1,
+        )
+
+    rooms_per_floor = schema.Int(
+        title = _(u'Rooms per Floor'),
+        required = True,
+        default = 2,
+        )
+
+    blocks_for_female = schema.List(
+        title = _(u'Blocks for Female Students'),
+        value_type = schema.Choice(
+            vocabulary = blocks
+            ),
+        defaultFactory=list,
+        )
+
+    blocks_for_male = schema.List(
+        title = _(u'Blocks for Male Students'),
+        value_type = schema.Choice(
+            vocabulary = blocks
+            ),
+        defaultFactory=list,
+        )
+
+    beds_for_pre= schema.List(
+        title = _(u'Beds for Pre-Study Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        defaultFactory=list,
+        )
+
+    beds_for_fresh = schema.List(
+        title = _(u'Beds for Fresh Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        defaultFactory=list,
+        )
+
+    beds_for_returning = schema.List(
+        title = _(u'Beds for Returning Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        defaultFactory=list,
+        )
+
+    beds_for_final = schema.List(
+        title = _(u'Beds for Final Year Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        defaultFactory=list,
+        )
+
+    beds_for_all = schema.List(
+        title = _(u'Beds without category'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        defaultFactory=list,
+        )
+
+    special_handling = schema.Choice(
+        title = _(u'Special Handling'),
+        source = SpecialHandlingSource(),
+        required = True,
+        default = u'regular',
+        )
+
+    maint_fee = schema.Float(
+        title = _(u'Rent'),
+        default = 0.0,
+        required = False,
+        )
+
+    @invariant
+    def blocksOverlap(hostel):
+        bfe = hostel.blocks_for_female
+        bma = hostel.blocks_for_male
+        if set(bfe).intersection(set(bma)):
+            raise Invalid(_('Female and male blocks overlap.'))
+
+    @invariant
+    def bedsOverlap(hostel):
+        beds = (hostel.beds_for_fresh +
+                hostel.beds_for_returning +
+                hostel.beds_for_final +
+                hostel.beds_for_pre +
+                hostel.beds_for_all)
+        if len(beds) != len(set(beds)):
+            raise Invalid(_('Bed categories overlap.'))
+
+    def writeLogMessage(view, message):
+        """Add an INFO message to hostels.log.
+        """
+
+class IBed(IKofaObject):
+    """Representation of a bed.
+    """
+
+    coordinates = Attribute('Coordinates tuple derived from bed_id')
+    hall = Attribute('Hall id, for exporter only')
+    block = Attribute('Block letter, for exporter only')
+    room = Attribute('Room number, for exporter only')
+    bed = Attribute('Bed letter, for exporter only')
+    special_handling = Attribute('Special handling code, for exporter only')
+    sex = Attribute('Sex, for exporter only')
+    bt = Attribute('Last part of bed type, for exporter only')
+
+    def bookBed(student_id):
+        """Book a bed for a student.
+        """
+
+    def switchReservation():
+        """Reserves bed or relases reserved bed respectively.
+        """
+
+    def releaseBedIfMaintenanceNotPaid():
+        """Release bed if maintenance fee has not been paid on time.
+        Reserve bed so that it cannot be automatically booked by someone else.
+        """
+
+    bed_id = schema.TextLine(
+        title = _(u'Bed Id'),
+        required = True,
+        default = u'',
+        )
+
+    bed_type = schema.TextLine(
+        title = _(u'Bed Type'),
+        required = True,
+        default = u'',
+        )
+
+    bed_number = schema.Int(
+        title = _(u'Bed Number'),
+        required = True,
+        )
+
+    owner = schema.TextLine(
+        title = _(u'Owner (Student)'),
+        description = _('Enter valid student id.'),
+        required = True,
+        default = u'',
+        )
+
+    @invariant
+    def allowed_owners(bed):
+        if bed.owner == NOT_OCCUPIED:
+            return
+        catalog = getUtility(ICatalog, name='students_catalog')
+        accommodation_session = getSite()['hostels'].accommodation_session
+        students = catalog.searchResults(current_session=(
+            accommodation_session,accommodation_session))
+        student_ids = [student.student_id for student in students]
+        if not bed.owner in student_ids:
+            raise Invalid(_(
+                "Either student does not exist or student "
+                "is not in accommodation session."))
+        catalog = getUtility(ICatalog, name='beds_catalog')
+        beds = catalog.searchResults(owner=(bed.owner,bed.owner))
+        if len(beds):
+            allocated_bed = [bed.bed_id for bed in beds][0]
+            raise Invalid(_(
+                "This student resides in bed ${a}.",
+                mapping = {'a':allocated_bed}))
+
+    def writeLogMessage(view, message):
+        """Add an INFO message to hostels.log.
+        """
+
+class IHostelsUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+
+    def getBedStatistics():
+        """Return bed statistics.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/permissions.py	(revision 16045)
@@ -0,0 +1,60 @@
+## $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
+##
+"""
+Permissions for the accommdation section.
+"""
+import grok
+
+# Accommodation section permissions
+
+class ViewHostels(grok.Permission):
+    """The ViewHostels permission is applied to all views of the
+    Accommodation Section. Users with this permission can view but not edit
+    data in the Accommodation Section.
+    """
+    grok.name('waeup.viewHostels')
+
+class ManageHostels(grok.Permission):
+    """The ManageHostels permission is applied to manage pages in the
+    Accommodation Section.
+    """
+    grok.name('waeup.manageHostels')
+
+class ExportAccommodationData(grok.Permission):
+    """Accommodation Officers don't have the general exportData
+    permission and are only allowed to export accommodation data
+    (accommodation payment tickets and bed tickets).
+    The ExportAccommodationData permission is only used to filter the
+    respective exporters in the ExportJobContainerJobConfig view.
+    """
+    grok.name('waeup.exportAccommodationData')
+
+# Site Roles
+class AccommodationOfficer(grok.Role):
+    """Accommodation Officers can view and manage hostels. They can also export
+    student accommodation data (filtered payment tickets and bed tickets).
+    They can't access the Data Center but see student data export buttons
+    in the Academic Section.
+    """
+    grok.name('waeup.AccommodationOfficer')
+    grok.title(u'Accommodation Officer')
+    grok.permissions('waeup.viewHostels',
+                     'waeup.manageHostels',
+                     'waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportAccommodationData')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_bed_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_bed_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_bed_data.csv	(revision 16045)
@@ -0,0 +1,6 @@
+hostel_id,bed_id,owner,reserved
+block-a-upper-hostel,block-a-upper-hostel_A_101_A,,0
+block-a-upper-hostel,block-a-upper-hostel_A_101_B,,1
+block-a-upper-hostel,block-a-upper-hostel_A_101_C,,2
+block-a-upper-hostel,block-a-upper-hostel_A_101_D,K1000000,0
+block-a-upper-hostel,block-a-upper-hostel_A_101_E,K999,0
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_hostel_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_hostel_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/sample_hostel_data.csv	(revision 16045)
@@ -0,0 +1,11 @@
+beds_for_all,beds_for_final,beds_for_fresh,beds_for_pre,beds_for_returning,blocks_for_female,blocks_for_male,floors_per_block,hostel_id,hostel_name,rooms_per_floor,sort_id,special_handling
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['A'],[],1,block-a-upper-hostel,Block A Upper Hostel,32,20,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['B'],[],1,block-b-upper-hostel,Block B Upper Hostel,32,30,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['C'],[],1,block-c-upper-hostel,Block C Upper Hostel,32,40,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['D'],[],1,block-d-upper-hostel,Block D Upper Hostel,32,50,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['E'],[],1,block-e-down-hostel,Block E Down Hostel,32,60,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['F'],[],1,block-f-down-hostel,Block F Down Hostel,32,70,regular
+[],"['A', 'B']","['E', 'F']",['G'],"['C', 'D']",['G'],[],1,block-g-down-hostel,Block G Down Hostel,32,80,regular
+[],"['A', 'B']","['D', 'E']",['F'],['C'],[],"['A', 'B']",1,hall-a,Hall A,44,90,regular
+[],"['A', 'B']","['D', 'E']",['F'],['C'],[],"['C', 'D']",1,hall-b,Hall B,44,99,regular
+[],[],[],[],[],[],['A'],1,stores-32,Stores 32,16,10,regular
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/tests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/tests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/tests.py	(revision 16045)
@@ -0,0 +1,701 @@
+## $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
+##
+"""
+Tests for hostels and their UI components.
+"""
+import os
+import shutil
+import tempfile
+import grok
+import pytz
+from datetime import datetime, timedelta
+from zope.event import notify
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component.hooks import setSite, clearSite
+from zope.testbrowser.testing import Browser
+from zope.security.interfaces import Unauthorized
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from waeup.kofa.app import University
+from waeup.kofa.hostels.interfaces import (
+    IHostelsContainer, IHostel, IBed, IHostelsUtils)
+from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
+from waeup.kofa.hostels.container import HostelsContainer
+from waeup.kofa.hostels.hostel import Hostel, Bed
+from waeup.kofa.hostels.batching import HostelProcessor, BedProcessor
+from waeup.kofa.hostels.export import BedExporter, HostelExporter
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+from waeup.kofa.students.student import Student
+from waeup.kofa.students.accommodation import BedTicket
+from waeup.kofa.university.department import Department
+
+HOSTEL_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_hostel_data.csv'),
+    'rb').read()
+
+HOSTEL_HEADER_FIELDS = HOSTEL_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+BED_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_bed_data.csv'),
+    'rb').read()
+
+BED_HEADER_FIELDS = BED_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class HostelsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IHostelsContainer, HostelsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IHostelsContainer, HostelsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IHostel, Hostel)
+            )
+        self.assertTrue(
+            verifyObject(
+                IHostel, Hostel())
+            )
+        self.assertTrue(
+            verifyClass(
+                IBed, Bed)
+            )
+        bed = Bed()
+        bed.bed_id = u'a_b_c_d'
+        bed.bed_type = u'a_b_c'
+        self.assertTrue(
+            verifyObject(
+                IBed, bed)
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = HostelsContainer()
+        hostel = Hostel()
+        # We cannot add arbitrary objects
+        department = Department()
+        self.assertRaises(
+            TypeError, container.addHostel, department)
+        self.assertRaises(
+            TypeError, hostel.addBed, department)
+        # Application is expired if startdate or enddate are not set
+        # or current datetime is outside application period.
+        self.assertTrue(container.expired)
+        delta = timedelta(days=10)
+        container.startdate = datetime.now(pytz.utc) - delta
+        self.assertTrue(container.expired)
+        container.enddate = datetime.now(pytz.utc) + delta
+        self.assertFalse(container.expired)
+
+class HostelsFullSetup(FunctionalTestCase):
+
+    def setUp(self):
+        super(HostelsFullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Add student with subobjects
+        student = Student()
+        student.firstname = u'Anna'
+        student.lastname = u'Tester'
+        student.reg_number = u'123'
+        student.matric_number = u'234'
+        student.sex = u'f'
+        self.app['students'].addStudent(student)
+        self.student_id = student.student_id
+        self.student = self.app['students'][self.student_id]
+        self.student['studycourse'].current_session = 2004
+        self.student['studycourse'].entry_session = 2004
+
+        # The students_catalog must be informed that the
+        # session attribute has changed
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        # Set accommodation_session
+        self.app['hostels'].accommodation_session = 2004
+
+        # Create a hostel
+        hostel = Hostel()
+        hostel.hostel_id = u'hall-x'
+        self.app['hostels'][hostel.hostel_id] = hostel
+
+        # Create a bed
+        bed = Bed()
+        bed.bed_id = u'hall_block_room_bed'
+        bed.bed_number = 1
+        bed.bed_type = u'regular_male_fr'
+        self.app['hostels'][hostel.hostel_id][bed.bed_id] = bed
+
+        self.container_path = 'http://localhost/app/hostels'
+        self.student_path = 'http://localhost/app/students/%s' % self.student_id
+        self.manage_container_path = self.container_path + '/@@manage'
+        self.add_hostel_path = self.container_path + '/addhostel'
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'hostels.log')
+
+    def tearDown(self):
+        super(HostelsFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+class HostelsContainerTests(HostelsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_release_expired_allocations(self):
+        self.app['hostels'].allocation_expiration = 7
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        bedticket = BedTicket()
+        bedticket.booking_session = 2004
+        bedticket.bed_coordinates = u'anything'
+        self.student['accommodation'].addBedTicket(bedticket)
+        self.app[
+            'hostels']['hall-x']['hall_block_room_bed'].owner = self.student_id
+        notify(grok.ObjectModifiedEvent(
+            self.app['hostels']['hall-x']['hall_block_room_bed']))
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall_block_room_bed'].bed_type,
+            'regular_male_fr')
+        results = cat.searchResults(owner=(self.student_id, self.student_id))
+        self.assertEqual(len(results), 1)
+        self.browser.open(self.container_path + '/releaseexpired')
+        self.assertTrue('Portal must be in maintenance mode for releasing expired bed allocations'
+            in self.browser.contents)
+        grok.getSite()['configuration'].maintmode_enabled_by = u'any_id'
+        self.browser.open(self.container_path + '/releaseexpired')
+        self.assertTrue('No bed released' in self.browser.contents)
+        delta = timedelta(days=10)
+        bedticket.booking_date = datetime.utcnow() - delta
+        self.browser.open(self.container_path + '/releaseexpired')
+        self.assertTrue(
+            'Successfully released beds: hall_block_room_bed (K1000000)'
+            in self.browser.contents)
+        results = cat.searchResults(owner=(self.student_id, self.student_id))
+        self.assertEqual(len(results), 0)
+        self.assertMatches(bedticket.display_coordinates,
+            '-- booking expired (2015-10-14 08:35:38 UTC) --')
+        # The owner has been removed and the bed reserved.
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall_block_room_bed'].owner,
+            NOT_OCCUPIED)
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall_block_room_bed'].bed_type,
+            'regular_male_reserved')
+        # Accommodation session can't be changed if hostels are not empty.
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="form.accommodation_session").value = ['2005']
+        self.browser.getControl("Save").click()
+        self.assertTrue(
+            'You can\'t change the booking session before clearing all hostels'
+            in self.browser.contents)
+        # Releasing is logged.
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'hostels.browser.ReleaseExpiredAllocationsPage - hostels - '
+            'released: hall_block_room_bed (K1000000)'
+            in logcontent)
+        return
+
+    def test_bed_statistics(self):
+        utils = getUtility(IHostelsUtils)
+        self.app['hostels']['hall-x'][
+            'hall_block_room_bed'].owner = NOT_OCCUPIED
+        notify(grok.ObjectModifiedEvent(
+            self.app['hostels']['hall-x']['hall_block_room_bed']))
+        stats = utils.getBedStatistics()
+        self.assertEqual(stats,
+            {'regular_male_fr': (0, 1, 1),
+             'regular_female_fi': (0, 0, 0),
+             'regular_male_re': (0, 0, 0),
+             'regular_female_fr': (0, 0, 0),
+             'regular_female_all': (0, 0, 0),
+             'regular_female_re': (0, 0, 0),
+             'regular_female_reserved': (0, 0, 0),
+             'regular_male_reserved': (0, 0, 0),
+             'regular_male_fi': (0, 0, 0),
+             'regular_male_all': (0, 0, 0)}
+             )
+        self.app[
+            'hostels']['hall-x']['hall_block_room_bed'].owner = self.student_id
+        notify(grok.ObjectModifiedEvent(
+            self.app['hostels']['hall-x']['hall_block_room_bed']))
+        stats = utils.getBedStatistics()
+        self.assertEqual(stats,
+            {'regular_male_fr': (1, 0, 1),
+             'regular_female_fi': (0, 0, 0),
+             'regular_male_re': (0, 0, 0),
+             'regular_female_fr': (0, 0, 0),
+             'regular_female_all': (0, 0, 0),
+             'regular_female_re': (0, 0, 0),
+             'regular_female_reserved': (0, 0, 0),
+             'regular_male_reserved': (0, 0, 0),
+             'regular_male_fi': (0, 0, 0),
+             'regular_male_all': (0, 0, 0)}
+             )
+
+class BedCatalogTests(HostelsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get a beds catalog if we wish
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        assert cat is not None
+
+    def test_search_by_type(self):
+        # We can find a certain bed
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(
+            bed_type=(u'regular_male_fr', u'regular_male_fr'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['hostels']['hall-x']['hall_block_room_bed']
+
+    def test_search_by_owner(self):
+        # We can find a certain bed
+        myobj = self.app['hostels']['hall-x']['hall_block_room_bed']
+        myobj.owner = u'abc'
+        notify(grok.ObjectModifiedEvent(myobj))
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(owner=(u'abc', u'abc'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['hostels']['hall-x']['hall_block_room_bed']
+
+class HostelsUITests(HostelsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can't access hostels containers
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_container_path)
+        return
+
+    def test_add_search_edit_delete_manage_hostels(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.container_path)
+        self.browser.getLink("Manage accommodation").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_container_path)
+        self.browser.getControl("Add hostel").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_hostel_path)
+        self.browser.getControl("Create hostel").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Hostel created' in self.browser.contents)
+        self.browser.open(self.container_path + '/addhostel')
+        self.browser.getControl("Create hostel").click()
+        self.assertTrue('The hostel already exists' in self.browser.contents)
+        hall = self.app['hostels']['hall-1']
+        hall.blocks_for_female = ['A','B']
+        self.browser.open(self.container_path + '/hall-1')
+        expected = '...<ul id="form.blocks_for_female" ><li>Block A</li>...'
+        self.assertMatches(expected,self.browser.contents)
+        self.browser.open(self.container_path + '/hall-1/manage')
+        self.browser.getControl(name="form.rooms_per_floor").value = '1'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        # Since the testbrowser does not support Javascrip the
+        # save action cleared the settings above and we have to set them
+        # again.
+        self.assertTrue(len(hall.blocks_for_female) == 0)
+        hall.blocks_for_female = ['A','B']
+        hall.beds_for_fresh = ['A']
+        hall.beds_for_returning = ['B']
+        hall.beds_for_final = ['C']
+        hall.beds_for_all = ['D','E']
+        self.browser.getControl("Update all beds").click()
+        self.assertTrue('Portal must be in maintenance mode for bed updates'
+            in self.browser.contents)
+        grok.getSite()['configuration'].maintmode_enabled_by = u'any_id'
+        self.browser.getControl("Update all beds").click()
+        expected = '...0 empty beds removed, 10 beds added, 0 occupied beds modified ()...'
+        self.assertMatches(expected,self.browser.contents)
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(
+            bed_type=('regular_female_all', 'regular_female_all'))
+        results = [(x.bed_id, x.bed_type) for x in results]
+        self.assertEqual(results,
+            [(u'hall-1_A_101_D', u'regular_female_all'),
+             (u'hall-1_A_101_E', u'regular_female_all'),
+             (u'hall-1_B_101_D', u'regular_female_all'),
+             (u'hall-1_B_101_E', u'regular_female_all')])
+        # Reserve beds.
+        self.browser.getControl("Switch reservation", index=0).click()
+        self.assertTrue('No item selected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='hall-1_A_101_A').selected = True
+        ctrl.getControl(value='hall-1_A_101_B').selected = True
+        ctrl.getControl(value='hall-1_A_101_C').selected = True
+        ctrl.getControl(value='hall-1_A_101_D').selected = True
+        self.browser.getControl("Switch reservation", index=0).click()
+        self.assertTrue('Successfully switched beds: hall-1_A_101_A (reserved)'
+            in self.browser.contents)
+        self.assertEqual(self.app['hostels']['hall-1'][
+            'hall-1_A_101_D'].bed_type, 'regular_female_reserved')
+        # The catalog has been updated.
+        results = cat.searchResults(
+            bed_type=('regular_female_all', 'regular_female_all'))
+        results = [(x.bed_id, x.bed_type) for x in results]
+        self.assertEqual(results,
+            [(u'hall-1_A_101_E', u'regular_female_all'),
+             (u'hall-1_B_101_D', u'regular_female_all'),
+             (u'hall-1_B_101_E', u'regular_female_all')])
+        results = cat.searchResults(
+            bed_type=('regular_female_reserved', 'regular_female_reserved'))
+        results = [(x.bed_id, x.bed_type) for x in results]
+        self.assertEqual(results,
+            [(u'hall-1_A_101_A', u'regular_female_reserved'),
+             (u'hall-1_A_101_B', u'regular_female_reserved'),
+             (u'hall-1_A_101_C', u'regular_female_reserved'),
+             (u'hall-1_A_101_D', u'regular_female_reserved')])
+        # Change hostel configuration with one bed booked.
+        hall['hall-1_A_101_E'].owner = u'anyid'
+        notify(grok.ObjectModifiedEvent(hall['hall-1_A_101_E']))
+        hall.beds_for_fresh = ['A', 'E']
+        hall.beds_for_all = ['D']
+        self.browser.getControl("Update all beds").click()
+        expected = '...9 empty beds removed, 9 beds added, 1 occupied beds modified...'
+        self.assertMatches(expected,self.browser.contents)
+        # Updating beds (including booked beds!) does update catalog.
+        results = cat.searchResults(
+            bed_type=('regular_female_all', 'regular_female_all'))
+        results = [(x.bed_id, x.bed_type) for x in results]
+        # The reservation of hall-1_A_101_D has been cancelled.
+        self.assertEqual(results,
+            [(u'hall-1_A_101_D', u'regular_female_all'),
+             (u'hall-1_B_101_D', u'regular_female_all')])
+        # Release bed which has previously been booked.
+        bedticket = BedTicket()
+        bedticket.booking_session = 2004
+        bedticket.bed_coordinates = u'anything'
+        self.student['accommodation'].addBedTicket(bedticket)
+        self.app['hostels']['hall-1']['hall-1_A_101_D'].owner = self.student_id
+        notify(grok.ObjectModifiedEvent(self.app['hostels']['hall-1']['hall-1_A_101_D']))
+        self.browser.open(self.container_path + '/hall-1/manage')
+        ctrl = self.browser.getControl(name='val_id')
+        self.browser.getControl("Release selected beds", index=0).click()
+        self.assertMatches("...No item selected...", self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='hall-1_A_101_D').selected = True
+        self.browser.getControl("Release selected beds", index=0).click()
+        self.assertMatches(
+          '...Successfully released beds: hall-1_A_101_D (%s)...' % self.student_id,
+          self.browser.contents)
+        self.assertMatches(bedticket.bed_coordinates,
+          u' -- booking cancelled on <YYYY-MM-DD hh:mm:ss> UTC --')
+        # The catalog has been updated.
+        results = cat.searchResults(owner=(self.student_id, self.student_id))
+        assert len(results) == 0
+        # If we release a free bed, nothing will happen.
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='hall-1_A_101_D').selected = True
+        self.browser.getControl("Release selected beds", index=0).click()
+        self.assertMatches(
+          '...No allocated bed selected...', self.browser.contents)
+        # Managers can manually allocate eligible students after cancellation.
+        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_A')
+        # 'not occupied' is not accepted.
+        self.browser.getControl("Save").click()
+        self.assertMatches(
+            "...No valid student id...",
+            self.browser.contents)
+        # Invalid student ids are not accepted.
+        self.browser.getControl(name="form.owner").value = 'nonsense'
+        self.browser.getControl("Save").click()
+        self.assertMatches(
+            "...Either student does not exist or student "
+            "is not in accommodation session...",
+            self.browser.contents)
+        self.browser.getControl(name="form.owner").value = self.student_id
+        self.browser.getControl("Save").click()
+        self.assertMatches("...Form has been saved...", self.browser.contents)
+        # Students can only be allocated once.
+        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_B')
+        self.browser.getControl(name="form.owner").value = self.student_id
+        self.browser.getControl("Save").click()
+        self.assertMatches(
+            "...This student resides in bed hall-1_A_101_A...",
+            self.browser.contents)
+        # If we open the same form again, we will be redirected to hostel
+        # manage page. Beds must be released first before they can be
+        # allocated to other students.
+        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_A')
+        self.assertEqual(self.browser.url,
+            self.container_path + '/hall-1/@@manage#tab2')
+        # Updating the beds again will not affect the allocation and also
+        # the bed numbering remains the same.
+        old_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
+        old_owner = self.app['hostels']['hall-1']['hall-1_A_101_A'].owner
+        self.browser.getControl("Update all beds").click()
+        # 8 beds have been removed and re-added, 2 beds remains untouched
+        # because they are occupied.
+        expected = '...8 empty beds removed, 8 beds added, 0 occupied beds modified...'
+        self.assertMatches(expected,self.browser.contents)
+        new_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
+        new_owner = self.app['hostels']['hall-1']['hall-1_A_101_A'].owner
+        self.assertEqual(new_number, old_number)
+        self.assertEqual(new_owner, old_owner)
+        # If we change the bed type of an allocated bed, the modification will
+        # be indicated.
+        hall.blocks_for_female = ['B']
+        hall.blocks_for_male = ['A']
+        self.browser.getControl("Update all beds").click()
+        expected = '...8 empty beds removed, 8 beds added, ' + \
+            '2 occupied beds modified (hall-1_A_101_A, hall-1_A_101_E, )...'
+        self.assertMatches(expected,self.browser.contents)
+        new_number = self.app['hostels']['hall-1']['hall-1_A_101_A'].bed_number
+        # Also the number of the bed has changed.
+        self.assertFalse(new_number == old_number)
+        # The number of occupied beds are displayed on container page.
+        self.browser.open(self.container_path)
+        self.assertTrue('2 of 10' in self.browser.contents)
+        bedticket.bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        # Managers can open the bed statistics page
+        self.browser.getLink("Bed statistics").click()
+        self.assertTrue('Bed Statistics</h1>' in self.browser.contents)
+        # Remove entire hostel.
+        self.browser.open(self.manage_container_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+        # Catalog is empty.
+        results = cat.searchResults(
+            bed_type=('regular_female_all', 'regular_female_all'))
+        results = [x for x in results]
+        assert len(results) == 0
+        # Bed has been removed from bedticket
+        self.assertEqual(bedticket.bed, None)
+        # Actions are logged.
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'hall-1 - 9 empty beds removed, 9 beds added, 1 occupied '
+            'beds modified (hall-1_A_101_E, )'
+            in logcontent)
+
+    def test_clear_hostels(self):
+        grok.getSite()['configuration'].maintmode_enabled_by = u'any_id'
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.container_path)
+        self.browser.getLink("Manage accommodation").click()
+        self.browser.getControl("Add hostel").click()
+        self.browser.getControl("Create hostel").click()
+        hall = self.app['hostels']['hall-1']
+        hall.blocks_for_female = ['A','B']
+        hall.rooms_per_floor = 1
+        hall.beds_for_fresh = ['A']
+        hall.beds_for_returning = ['B']
+        hall.beds_for_final = ['C']
+        hall.beds_for_all = ['D','E']
+        self.browser.open(self.container_path + '/hall-1/manage')
+        self.browser.getControl("Update all beds").click()
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(bed_type=(None, None))
+        self.assertEqual(len(results), 11)
+        self.browser.getControl("Clear hostel").click()
+        self.assertEqual(len(self.app['hostels']['hall-1']), 0)
+        # Only the bed in hall-x remains in the catalog.
+        results = cat.searchResults(bed_type=(None, None))
+        self.assertEqual(len(results), 1)
+        # We can clear all hostels at the same time.
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl("Clear all hostels").click()
+        results = cat.searchResults(bed_type=(None, None))
+        self.assertEqual(len(results), 0)
+        # Both actions have been logged.
+        logcontent = open(self.logfile).read()
+        self.assertTrue('INFO - zope.mgr - hostels.browser.HostelManageFormPage'
+                        ' - hall-1 - cleared' in logcontent)
+        self.assertTrue('zope.mgr - hostels.browser.HostelsContainerManagePage'
+                        ' - hostels - all hostels cleared' in logcontent)
+
+class ExportTests(HostelsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ExportTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def test_export_hostels(self):
+        exporter = HostelExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'beds_for_all,beds_for_final,beds_for_fresh,beds_for_pre,'
+            'beds_for_returning,blocks_for_female,'
+            'blocks_for_male,floors_per_block,hostel_id,hostel_name,maint_fee,'
+            'rooms_per_floor,sort_id,special_handling\r\n[],[],[],[],[],[],[],'
+            '1,hall-x,Hall 1,0.0,2,10,regular\r\n'
+            )
+        return
+
+    def test_export_beds(self):
+        exporter = BedExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'bed_id,bed_number,bed_type,owner,hall,block,room,bed,'
+            'special_handling,sex,bt\r\nhall_block_room_bed,1,regular_male_fr,,'
+            'hall,block,room,bed,regular,male,fr\r\n'
+            )
+        return
+
+    def tearDown(self):
+        super(ExportTests, self).tearDown()
+        clearSite()
+        shutil.rmtree(os.path.dirname(self.outfile))
+
+class HostelBedProcessorTest(HostelsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_import(self):
+        self.processor = HostelProcessor()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sample_hostel_data.csv')
+        open(self.csv_file, 'wb').write(HOSTEL_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, HOSTEL_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        self.assertEqual(len(self.app['hostels'].keys()), 11) # including hall-x
+        self.assertEqual(self.app['hostels'][
+            'block-a-upper-hostel'].hostel_id,'block-a-upper-hostel')
+        self.assertEqual(self.app['hostels'][
+            'block-a-upper-hostel'].beds_for_final, ['A', 'B'])
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            "Hostel Processor - sample_hostel_data - block-a-upper-hostel - "
+            "updated: "
+            "beds_for_pre=['G'], floors_per_block=1, "
+            "special_handling=regular, "
+            "beds_for_final=['A', 'B'], rooms_per_floor=32, "
+            "hostel_id=block-a-upper-hostel, "
+            "sort_id=20, beds_for_returning=['C', 'D'], "
+            "hostel_name=Block A Upper Hostel, beds_for_fresh=['E', 'F'], "
+            "blocks_for_female=['A']"
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        shutil.rmtree(self.workdir)
+        # The manage page can be opened.
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.container_path + '/block-a-upper-hostel')
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_import_update_hostel_and_beds(self):
+        self.processor = HostelProcessor()
+        self.workdir = tempfile.mkdtemp()
+        self.csv_file = os.path.join(self.workdir, 'sample_hostel_data.csv')
+        open(self.csv_file, 'wb').write(HOSTEL_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, HOSTEL_HEADER_FIELDS)
+        # We import the same file in update mode
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, HOSTEL_HEADER_FIELDS, 'update')
+        self.assertEqual(num_warns,0)
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            "Hostel Processor - sample_hostel_data - block-a-upper-hostel - "
+            "updated: "
+            "beds_for_pre=['G'], floors_per_block=1, "
+            "special_handling=regular, "
+            "beds_for_final=['A', 'B'], rooms_per_floor=32, "
+            "hostel_id=block-a-upper-hostel, "
+            "sort_id=20, beds_for_returning=['C', 'D'], "
+            "hostel_name=Block A Upper Hostel, beds_for_fresh=['E', 'F'], "
+            "blocks_for_female=['A']"
+            in logcontent)
+        # Update the beds of first hostel
+        self.app['hostels']['block-a-upper-hostel'].updateBeds()
+        # Import beds
+        self.processor = BedProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_bed_data.csv')
+        open(self.csv_file, 'wb').write(BED_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, BED_HEADER_FIELDS, 'update')
+        self.assertEqual(num_warns,2)
+        fail_file_content = open(fail_file).read()
+        logcontent = open(self.logfile).read()
+        self.assertEqual(
+            'owner,bed_id,hostel_id,reserved,--ERRORS--\r\n'
+            '<IGNORE>,block-a-upper-hostel_A_101_C,block-a-upper-hostel,2,'
+            'reserved: invalid value\r\n'
+            'K999,block-a-upper-hostel_A_101_E,block-a-upper-hostel,0,'
+            'owner: student does not exist\r\n',
+            fail_file_content)
+        self.assertTrue(
+            'system - Bed Processor (update only) - sample_bed_data - '
+            'block-a-upper-hostel_A_101_A - updated: nothing'
+            in logcontent)
+        self.assertTrue(
+            'system - Bed Processor (update only) - sample_bed_data - '
+            'block-a-upper-hostel_A_101_B - updated: reserved'
+            in logcontent)
+        self.assertTrue(
+            'system - Bed Processor (update only) - sample_bed_data - '
+            'block-a-upper-hostel_A_101_D - updated: owner=K1000000'
+            in logcontent)
+        self.assertEqual(
+            self.app['hostels']['block-a-upper-hostel']
+                    ['block-a-upper-hostel_A_101_D'].owner, 'K1000000')
+        self.assertEqual(
+            self.app['hostels']['block-a-upper-hostel']
+                    ['block-a-upper-hostel_A_101_B'].bed_type,
+                    'regular_female_reserved')
+        shutil.rmtree(os.path.dirname(fin_file))
+        shutil.rmtree(self.workdir)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/utils.py	(revision 16045)
@@ -0,0 +1,57 @@
+## $Id$
+##
+## Copyright (C) 2015 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
+##
+"""General helper functions and utilities for the hostels section.
+"""
+
+import grok
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.hostels.interfaces import IHostelsUtils
+from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
+
+class HostelsUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(IHostelsUtils)
+
+    def getBedStatistics(self):
+        cat = getUtility(ICatalog, name='beds_catalog')
+        stats = {}
+        bed_types = (
+            'regular_female_fr',
+            'regular_female_re',
+            'regular_female_fi',
+            'regular_female_all',
+            'regular_female_reserved',
+            'regular_male_fr',
+            'regular_male_re',
+            'regular_male_fi',
+            'regular_male_all',
+            'regular_male_reserved',)
+        for bed_type in bed_types:
+            free = cat.searchResults(
+                bed_type=(bed_type, bed_type),
+                owner=(NOT_OCCUPIED, NOT_OCCUPIED))
+            all = cat.searchResults(
+                bed_type=(bed_type, bed_type))
+            free = len(free)
+            total = len(all)
+            occ = total - free
+            stats[bed_type] = (occ, free, total)
+        return stats
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/vocabularies.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/vocabularies.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/hostels/vocabularies.py	(revision 16045)
@@ -0,0 +1,76 @@
+## $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
+##
+"""Vocabularies and sources for the accommodation section.
+"""
+from  grok import getSite
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.kofa.interfaces import (
+    SimpleKofaVocabulary, ContextualDictSourceFactoryBase)
+from waeup.kofa.interfaces import MessageFactory as _
+
+NOT_OCCUPIED = u'not occupied'
+
+class SpecialHandlingSource(ContextualDictSourceFactoryBase):
+    """A application category source delivers all special handling categories
+    provided for accommodation booking.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'SPECIAL_HANDLING_DICT'
+
+bed_letters = SimpleKofaVocabulary(
+    (_('Bed A'),'A'),
+    (_('Bed B'),'B'),
+    (_('Bed C'),'C'),
+    (_('Bed D'),'D'),
+    (_('Bed E'),'E'),
+    (_('Bed F'),'F'),
+    (_('Bed G'),'G'),
+    (_('Bed H'),'H'),
+    (_('Bed I'),'I'),
+    (_('Bed J'),'J'),
+    (_('Bed K'),'K'),
+    (_('Bed L'),'L'),
+    )
+
+blocks = SimpleKofaVocabulary(
+    (_('Block A'),'A'),
+    (_('Block B'),'B'),
+    (_('Block C'),'C'),
+    (_('Block D'),'D'),
+    (_('Block E'),'E'),
+    (_('Block F'),'F'),
+    (_('Block G'),'G'),
+    (_('Block H'),'H'),
+    (_('Block I'),'I'),
+    (_('Block J'),'J'),
+    (_('Block K'),'K'),
+    (_('Block L'),'L'),
+    (_('Block M'),'M'),
+    (_('Block N'),'N'),
+    (_('Block O'),'O'),
+    (_('Block P'),'P'),
+    (_('Block Q'),'Q'),
+    (_('Block R'),'R'),
+    (_('Block S'),'S'),
+    (_('Block T'),'T'),
+    (_('Block U'),'U'),
+    (_('Block V'),'V'),
+    (_('Block W'),'W'),
+    )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/README.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/README.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/README.txt	(revision 16045)
@@ -0,0 +1,216 @@
+waeup.kofa.image -- handling image files
+========================================
+
+The image file widget is built on top of the :class:`KofaImageFile` object::
+
+  >>> from waeup.kofa.image import KofaImageFile
+  >>> file = KofaImageFile('foo.jpg', 'mydata')
+  >>> file.filename
+  'foo.jpg'
+  >>> file.data
+  'mydata'
+  >>> file.size
+  6
+  >>> f = file.file
+  >>> f.read()
+  'mydata'
+
+We can also create KofaImageFile objects from file-like objects::
+
+  >>> from StringIO import StringIO
+  >>> from zope import component
+  >>> from hurry.file.interfaces import IFileRetrieval
+  >>> fileretrieval = component.getUtility(IFileRetrieval)
+  >>> file = fileretrieval.createFile('bar.jpg', StringIO('test data'))
+  >>> file.filename
+  'bar.jpg'
+  >>> file.size
+  9
+  >>> file.data
+  'test data'
+  >>> f = file.file
+  >>> f.read()
+  'test data'
+
+Tramline Support
+----------------
+
+The KofaImageFile object normally stores the file data using ZODB
+persistence. Files can however also be stored by tramline.  If
+tramline is installed in Apache, the Tramline takes care of generating
+ids for files and storing the file on the filesystem directly. The ids
+are then passed as file data to be stored in the ZODB.
+
+Let's first enable tramline.
+
+The tramline directory structure is a directory with two subdirectories,
+one called 'repository' and the other called 'upload'::
+
+  >>> import tempfile, os
+  >>> dirpath = tempfile.mkdtemp()
+  >>> repositorypath = os.path.join(dirpath, 'repository')
+  >>> uploadpath = os.path.join(dirpath, 'upload')
+  >>> os.mkdir(repositorypath)
+  >>> os.mkdir(uploadpath)
+
+We create a TramlineFileRetrieval object knowing about this directory,
+and register it as a utility::
+
+  >>> from hurry.file.file import TramlineFileRetrievalBase
+  >>> class TramlineFileRetrieval(TramlineFileRetrievalBase):
+  ...    def getTramlinePath(self):
+  ...        return dirpath
+  >>> retrieval = TramlineFileRetrieval()
+  >>> component.provideUtility(retrieval, IFileRetrieval)
+
+Now let's store a file the way tramline would during upload::
+
+  >>> f = open(os.path.join(repositorypath, '1'), 'wb')
+  >>> f.write('test data')
+  >>> f.close()
+
+The file with underlying name '1' (the data stored in the ZODB will be
+just '1') will now be created::
+
+  >>> file = KofaImageFile('foo.jpg', '1')
+
+The data is now '1', referring to the real file::
+
+  >>> file.data
+  '1'
+
+Retrieving the file results in the real file::
+
+  >>> f = file.file
+  >>> f.read()
+  'test data'
+
+We can also retrieve its size::
+
+  >>> int(file.size)
+  9
+
+Now let's disable tramline in our utility::
+
+  >>> class TramlineFileRetrieval(TramlineFileRetrievalBase):
+  ...     def getTramlinePath(self):
+  ...        return dirpath
+  ...     def isTramlineEnabled(self):
+  ...        return False
+  >>> component.provideUtility(TramlineFileRetrieval(), IFileRetrieval)
+
+We expect the same behavior as when tramline is not installed::
+
+  >>> file = KofaImageFile('foo.jpg', 'data')
+  >>> f = file.file
+  >>> f.read()
+  'data'
+  >>> file.size
+  4
+
+Clean up:
+
+  >>> import shutil
+  >>> shutil.rmtree(dirpath)
+
+Support for :mod:`waeup.kofa.imagestorage`
+------------------------------------------
+
+The behaviour shown above can be used for any Zope3 application. With
+:mod:`waeup.kofa` we use a special file retrieval utility defined in
+:mod:`waeup.kofa.imagestorage`. As this utility is based on
+:mod:`waeup.kofa` internal stuff we do not show it here but in the
+tests that come with that storage type.
+
+Put roughly, an imagestorage stores file data in containers of blobs
+belonging to a certain site. See the module itself for details.
+
+`ImageFile` Field
+-----------------
+
+As every other field (most of them are defined in :mod:`zope.schema`)
+`ImageFile` can be used in interfaces to tell, that the associated
+attribute should be -- an image file.
+
+The `ImageFile` field accepts only certain content types:
+
+  >>> from waeup.kofa.image.schema import ImageFile
+  >>> from zope.publisher.browser import TestRequest
+  >>> field = ImageFile(__name__='foo', title=u'Foo')
+  >>> field.validate('asd')
+  Traceback (most recent call last):
+  ...
+  WrongType: ('asd', <class 'waeup.kofa.image.image.KofaImageFile'>, 'foo')
+
+which means: `ImageFile` fields should better contain
+:class:`KofaImageFile` instances.
+
+We can store normal :class:`KofaImageFile` instances:
+
+  >>> field.validate(KofaImageFile('bar.jpg', 'data')) is None
+  True
+
+The `ImageFile` field supports min and max values:
+
+  >>> field = ImageFile(__name__='foo', title=u'Foo')
+  >>> hasattr(field, 'min_size')
+  True
+
+  >>> hasattr(field, 'max_size')
+  True
+
+By default both attributes are set to ``None``:
+
+  >>> field.min_size is field.max_size is None
+  True
+
+But we can set them:
+
+  >>> field = ImageFile(__name__='bar', title=u'Bar',
+  ...                   min_size=5, max_size=12)
+
+and while these values are okay then:
+
+  >>> field.validate(
+  ...     KofaImageFile('bar.jpg', '123456789012')) is None
+  True
+
+  >>> field.validate(
+  ...     KofaImageFile('bar.jpg', '12345')) is None
+  True
+
+the following are not:
+
+  >>> field.validate(
+  ...     KofaImageFile('bar.jpg', '1234567890123'))
+  Traceback (most recent call last):
+  ...
+  TooBig: ('bar.jpg', '13 bytes (max: 12 bytes)')
+
+  >>> field.validate(
+  ...     KofaImageFile('bar.jpg', '1234'))
+  Traceback (most recent call last):
+  ...
+  TooSmall: ('bar.jpg', '4 bytes (min: 5 bytes)')
+
+
+Broken Unequality Comparison
+----------------------------
+
+KofaImageFile does not reproduce the broken unequal comparison from
+its base:
+
+  >>> f1 = KofaImageFile('bar.jpg', '123456789')
+  >>> f2 = KofaImageFile('bar.jpg', '123456789')
+  >>> f3 = KofaImageFile('baz.jpg', '1234')
+  >>> f1 == f2
+  True
+
+  >>> f1 != f2
+  False
+
+  >>> f1 == f3
+  False
+
+  >>> f1 != f3
+  True
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/__init__.py	(revision 16045)
@@ -0,0 +1,10 @@
+"""Image file components.
+
+Including schemas, widgets and the content components.
+"""
+from waeup.kofa.image.image import KofaImageFile, createKofaImageFile
+
+__all__ = [
+    "KofaImageFile",
+    "createKofaImageFile",
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/__init__.py	(revision 16045)
@@ -0,0 +1,10 @@
+# The public API part
+# Contains the widgets defined for image files.
+from waeup.kofa.image.browser.widget import (
+    EncodingImageFileWidget, ThumbnailWidget,
+    )
+
+__all__ = (
+    EncodingImageFileWidget,
+    ThumbnailWidget,
+    )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/image.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/image.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/image.txt	(revision 16045)
@@ -0,0 +1,421 @@
+Image Widgets
+=============
+
+This is an infrastructure to create an image file widget that behaves
+as much as possible like a normal text widget in formlib. Normally a
+file widget loses its file data when a form is re-presented for
+reasons of failing form validation. A ``hurry.file`` widget retains
+the file, for example by storing it in a session.
+
+In order to do this, we have a special way to store file data along with
+its filename:
+
+  >>> import os
+  >>> from waeup.kofa.image import KofaImageFile
+  >>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg')
+  >>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg')
+  >>> some_file = KofaImageFile('foo.jpg', open(testimage, 'rb').read())
+  >>> some_file.filename
+  'foo.jpg'
+  >>> 'Created with GIMP' in some_file.data
+  True
+
+We can provide a download widget. In this case, there's nothing
+to download:
+
+  >>> from hurry.file.browser import DownloadWidget
+  >>> from hurry.file.schema import File
+  >>> from waeup.kofa.image.schema import ImageFile
+  >>> from zope.publisher.browser import TestRequest
+  >>> field = ImageFile(__name__='foo', title=u'Foo')
+  >>> field = field.bind(None)
+  >>> request = TestRequest()
+  >>> widget = DownloadWidget(field, request)
+  >>> widget()
+  u'<div>Download not available</div>'
+
+Even if there were data in the request, there'd be nothing to download:
+
+  >>> from zope.publisher.browser import FileUpload
+  >>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
+  >>> widget = DownloadWidget(field, request)
+  >>> widget()
+  u'<div>Download not available</div>'
+
+Now set a value:
+
+  >>> widget.setRenderedValue(some_file)
+  >>> widget()
+  u'<a href="foo.jpg">foo.jpg</a>'
+
+Instead of downloading, we can also use a thumbnail widget:
+
+  >>> from waeup.kofa.image.browser import ThumbnailWidget
+  >>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
+  >>> widget = ThumbnailWidget(field, request)
+  >>> widget.setRenderedValue(some_file)
+  >>> widget()
+  u'<img src="foo.jpg" />'
+
+Now on to an edit widget. First the case in an add form with no
+data already available, and no data in request:
+
+  >>> from waeup.kofa.image.browser import EncodingImageFileWidget
+  >>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
+  >>> field = field.bind(None)
+  >>> request = TestRequest()
+  >>> widget = EncodingImageFileWidget(field, request)
+
+  >>> def normalize(s):
+  ...   return u'\n  '.join(filter(None, s.split(' ')))
+
+  >>> print normalize(widget())
+  <input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+
+Now let's try a situation where data is available in the request, but
+it's an empty string for the file:
+
+  >>> request = TestRequest(form={'field.foo': u''})
+  >>> widget = EncodingImageFileWidget(field, request)
+
+  >>> def normalize(s):
+  ...   return '\n  '.join(filter(None, s.split(' ')))
+
+  >>> print normalize(widget())
+  <input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+
+Now let's render again when there's already available data. What should show
+up is an extra, hidden field which contains the file_id:
+
+  >>> widget.setRenderedValue(some_file)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (foo.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="Zm9vLmpw...="
+    />
+
+Now let's render again, this time with file data available in the request
+instead. The same should happen:
+
+  >>> request = TestRequest(form={'field.foo': FileUpload(some_file)})
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (foo.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="Zm9vLmpwZ...="
+    />
+
+Now let's render again, this time not with file data available in the
+request, but an id. Again, we should see the same:
+
+  >>> request = TestRequest(form={'field.foo.file_id':
+  ...                             'Zm9vLnR4dAp0aGUgY29udGVudHM='})
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (foo.txt)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="Zm9vLnR4dAp0aGUgY29udGVudHM="
+    />
+
+If there is both file data and an id, something else happens. First, let's
+prepare some new file:
+
+  >>> another_file = KofaImageFile('bar.txt', 'bar contents')
+
+We happen to know, due to the implementation of
+EncodingImageFileWidget, that the file_id is going to be
+"Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original
+id, but a new file upload:
+
+  >>> request = TestRequest(form={'field.foo': FileUpload(another_file),
+  ...                             'field.foo.file_id':
+  ...                             'Zm9vLnR4dAp0aGUgY29udGVudHM='})
+
+We expect the new file to be the one that's uploaded:
+
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (bar.txt)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="YmFyLnR4dApiYXIgY29udGVudHM="
+    />
+
+Support for File Retrievals
+---------------------------
+
+As :class:`waeup.kofa.image.KofaImageFile` objects support storing
+image data by using external 'storages', also our widgets should do
+so.
+
+We create a simple IFileRetrieval utility and enable it:
+
+  >>> import hashlib
+  >>> from zope.component import provideUtility
+  >>> from hurry.file.interfaces import IFileRetrieval
+  >>> from StringIO import StringIO
+  >>> class MyFileRetrieval(object):
+  ...     storage = dict()
+  ...     def getFile(self, data):
+  ...         entry = self.storage.get(data, None)
+  ...         if entry is None:
+  ...             return None
+  ...         return StringIO(entry)
+  ...     def createFile(self, filename, f):
+  ...         contents = f.read()
+  ...         id_string = hashlib.md5(contents).hexdigest()
+  ...         result = KofaImageFile(filename, id_string)
+  ...         self.storage[id_string] = contents
+  ...         return result
+
+  >>> retrieval = MyFileRetrieval()
+  >>> provideUtility(retrieval, IFileRetrieval)
+
+With this utility in place we can post requests. When no data was
+posted and the field also contains no data, we will get a simple
+input as appropriate for add forms:
+
+  >>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
+  >>> field = field.bind(None)
+  >>> request = TestRequest()
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+
+If the request contains empty data but it is only an empty string, the
+result will be the same:
+
+  >>> request = TestRequest(form={'field.foo': u''})
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+
+We now want to simulate, that the field contains already data,
+identified by some `file_id`. To do so, we first store the data in our
+file retrieval and then create a KofaImageFile object with that
+file_id stored:
+
+  >>> from waeup.kofa.image import createKofaImageFile
+  >>> image = createKofaImageFile(
+  ...     'sample.jpg', open(testimage, 'rb'))
+  >>> file_id = image.data
+  >>> file_id    # MD5 sum of the file contents
+  '9feac4265077922000aa8b88748e25be'
+
+  >>> import hashlib
+  >>> hashlib.md5(open(testimage, 'rb').read()).hexdigest()
+  '9feac4265077922000aa8b88748e25be'
+
+The new file was stored by our utility, as createKofaImageFile looks
+up IFileRetrieval utilities and uses them:
+
+  >>> retrieval.storage.keys()
+  ['9feac4265077922000aa8b88748e25be']
+
+We set this image as value of the widget:
+
+  >>> widget.setRenderedValue(image)
+  >>> print normalize(widget())
+  <img
+    src="sample.jpg"
+    /><br
+    /><input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (sample.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=="
+    />
+
+  >>> retrieval.storage.keys()
+  ['9feac4265077922000aa8b88748e25be']
+
+The stored hidden value contains the filename and our file_id:
+
+  >>> "c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ==".decode(
+  ...    'base64')
+  'sample.jpg\n9feac4265077922000aa8b88748e25be'
+
+Now, we want the the widget rendered again but this time with the data
+coming from the request. To do so, we have to create a FileUpload
+object that wraps the real file:
+
+  >>> class UploadedFile(object):
+  ...   headers = {}
+  ...   def __init__(self, filename, f):
+  ...     self.filename = filename
+  ...     self.file = f
+
+Now we can 'post' the request and will get the same result as above:
+
+  >>> upload = FileUpload(UploadedFile('sample.jpg', open(testimage, 'rb')))
+  >>> request = TestRequest(form={'field.foo': upload})
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (sample.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=="
+    />
+
+Now let's render again, this time not with file data available in the
+request, but an id. Again, we should see the same:
+
+  >>> request = TestRequest(form={
+  ...   'field.foo.file_id':
+  ...   'c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=='})
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (sample.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="c2FtcGxlLmpwZwo5ZmVhYzQyNjUwNzc5MjIwMDBhYThiODg3NDhlMjViZQ=="
+    />
+
+If there is both file data and an id, something else happens. First, let's
+prepare some new file:
+
+  >>> upload = FileUpload(UploadedFile('sample2.jpg', open(testimage2, 'rb')))
+
+We happen to know, due to the implementation of
+EncodingImageFileWidget, that the file_id is going to be
+"Zm9vLnR4dAp0aGUgY29udGVudHM=". Let's make a request with the original
+id, but a new file upload:
+
+  >>> request = TestRequest(form={'field.foo': upload,
+  ...                             'field.foo.file_id':
+  ...                             'Z2FtcGxlLmpwZwr/2P/gAA=='})
+
+We expect the new file to be the one that's uploaded:
+
+  >>> field = ImageFile(__name__='foo', title=u'Foo', required=False)
+  >>> field = field.bind(None)
+  >>> widget = EncodingImageFileWidget(field, request)
+  >>> print normalize(widget())
+  <img... />...
+  ...<input
+    class="fileType"
+    id="field.foo"
+    name="field.foo"
+    size="20"
+    type="file"
+    />
+    (sample2.jpg)<input
+    class="hiddenType"
+    id="field.foo.file_id"
+    name="field.foo.file_id"
+    type="hidden"
+    value="c2FtcGxlMi5qcGcKZDk2MDkwZWRlMmRjODlkZDdkZWM5ZDU3MmFkNThjNzQ="
+    />
+
+The value displayed above again means the filename and md5 sum of the
+stored file:
+
+  >>> "c2FtcGxlMi5qcGcKZDk2MDkwZWRlMmRjODlkZDdkZWM5ZDU3MmFkNThjNzQ=".decode(
+  ...      'base64')
+  'sample2.jpg\nd96090ede2dc89dd7dec9d572ad58c74'
+
+where the md5 sum is in fact correct:
+
+  >>> import hashlib
+  >>> hashlib.md5(open(testimage2, 'rb').read()).hexdigest()
+  'd96090ede2dc89dd7dec9d572ad58c74'
+
+Our file retrieval utility now contains two files:
+
+  >>> retrieval.storage.keys()
+  ['9feac4265077922000aa8b88748e25be', 'd96090ede2dc89dd7dec9d572ad58c74']
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_image.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_image.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_image.py	(revision 16045)
@@ -0,0 +1,47 @@
+##
+## tests.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Sep 13 10:38:33 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 Uli Fouquet
+## 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
+##
+"""Tests for image files.
+"""
+import doctest
+import unittest
+
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+from zope import component
+from zope.app.testing import placelesssetup
+
+
+def imageSetUp(doctest):
+    placelesssetup.setUp()
+    component.provideUtility(IdFileRetrieval(), IFileRetrieval)
+
+def test_suite():
+    return unittest.TestSuite(
+        (
+            doctest.DocFileSuite(
+                'image.txt',
+                setUp=imageSetUp,
+                tearDown=placelesssetup.tearDown,
+                optionflags=doctest.ELLIPSIS,
+                ),
+            )
+        )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_views.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_views.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/tests/test_views.py	(revision 16045)
@@ -0,0 +1,92 @@
+##
+## test_views.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Apr 11 22:45:10 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## 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 image file views.
+"""
+import grok
+import os
+import unittest
+from tempfile import mkstemp
+from hurry.file import HurryFile
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+from waeup.kofa.image.browser.views import HurryFileView
+from zope.component import getMultiAdapter, provideUtility
+from zope.publisher.browser import TestRequest
+
+# List of expected input-ouput: (name, expected-mimetype)
+CONTENT_TYPES = (
+    ('jpg', 'image/jpeg'),
+    )
+
+class ImageBrowserViewsLayer(object):
+    """A layer that registers all components in
+       `waeup.kofa.image.browser.views` module.
+    """
+    @classmethod
+    def setUp(cls):
+        grok.testing.grok('waeup.kofa.image.browser.views')
+
+    @classmethod
+    def tearDown(cls):
+        pass
+
+class HurryFileViewTestCase(unittest.TestCase):
+
+    layer = ImageBrowserViewsLayer
+
+    def setUp(self):
+        # Create a bunch of files and `HurryFile` objects pointing to
+        # them.  Also create a dict of content types expected. Each of
+        # the dicts indexed by the name of the above CONTENT_TYPES.
+        self.filenames = dict()
+        self.files = dict()
+        self.content_types = dict()
+        for name, content_type in CONTENT_TYPES:
+            fd, filename = mkstemp(suffix='.%s' % name)
+            self.filenames[name] = filename
+            self.files[name] = HurryFile(os.path.basename(filename), fd)
+            self.content_types[name] = content_type
+        self.request = TestRequest()
+        # Now create the most simple fileretrieval possible
+        retrieval = IdFileRetrieval()
+        provideUtility(retrieval, IFileRetrieval)
+        return
+
+    def tearDown(self):
+        # Remove all temporary files.
+        for filename in self.filenames.values():
+            os.unlink(filename)
+        return
+
+    def test_mimetype_jpeg(self):
+        # Create a view of some JPEG file, call that view and see what
+        # mimetype we get.
+        view = getMultiAdapter(
+            (self.files['jpg'], self.request), name='index.html'
+            )
+        view() # Call the view in order to set the headers...
+        response = self.request.response
+        content_type = response.getHeader('Content-Type')
+        self.assertEqual(
+            content_type, self.content_types['jpg']
+            )
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/views.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/views.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/views.py	(revision 16045)
@@ -0,0 +1,48 @@
+## $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
+##
+"""Views for images and the like.
+"""
+import mimetypes
+import grok
+from hurry.file.interfaces import IHurryFile
+
+class HurryFileView(grok.View):
+    """A view for hurry files stored in the ZODB.
+
+    Normally these files cannot be 'browsed' but with a view we can
+    stream them to the browser. We only have to set the appropriate
+    MIME type, which happens in the `render` method.
+    """
+
+    grok.name('index.html')
+    grok.context(IHurryFile)
+
+    def render(self):
+        """Output the data of context.
+
+        Beside this we set the output MIME type according to the
+        context's filename.
+        """
+        content_type, encoding = mimetypes.guess_type(
+            self.context.filename.lower())
+        if content_type is not None:
+            self.response.setHeader('Content-Type', content_type)
+        fd = self.context.file
+        contents = fd.read()
+        fd.close()
+        return contents
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/widget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/widget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/browser/widget.py	(revision 16045)
@@ -0,0 +1,166 @@
+## $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
+##
+"""Image file widgets.
+"""
+import os
+from waeup.kofa.image import KofaImageFile
+from hurry.file.browser.widget import (
+    EncodingFileWidget, DownloadWidget, FakeFieldStorage)
+from hurry.file.interfaces import IFileRetrieval
+from zope.app.form.browser.textwidgets import escape
+from zope.app.form.browser.widget import renderElement
+from zope.app.form.interfaces import ConversionError
+from zope.component import queryUtility
+from zope.publisher.browser import FileUpload
+
+
+class EncodingImageFileWidget(EncodingFileWidget):
+    """A hurry.file widget that stores images.
+
+    This is an upload widget suitable for edit forms and the like.
+    """
+    #: We can set a basename for the image to be rendered.
+    #: This will result in <img> links like <img src='.../<basename>.jpg' />
+    image_basename = None
+
+    def _toFieldValue(self, (input, file_id)):
+        # we got no file upload input
+        if not input:
+            # if we got a file_id, then retrieve file and return it
+            if file_id:
+                return self._retrieveFile(file_id)
+            # no file upload input nor file id, so return missing value
+            return self.context.missing_value
+        # read in file from input
+        try:
+            seek = input.seek
+            read = input.read
+        except AttributeError, e:
+            raise ConversionError('Form input is not a file object', e)
+
+        seek(0)
+        data = read()
+
+        if data:
+            retrieval = queryUtility(IFileRetrieval)
+            if retrieval is not None:
+                seek(0)
+                return retrieval.createFile(input.filename, input)
+            return KofaImageFile(input.filename, data)
+        else:
+            return self.context.missing_value
+
+    def _toFormValue(self, value):
+        if value == self.context.missing_value:
+            return self._missing
+        data = value.file.read()
+        return FileUpload(FakeFieldStorage(
+                value.filename.encode('UTF-8'), data))
+
+    def __call__(self):
+        value = self._getFormValue()
+        if value:
+            file_id = self._setFile(value)
+        else:
+            file_id = None
+        result = u''
+        options = dict(
+            type=self.type,
+            name=self.name,
+            id=self.name,
+            cssClass=self.cssClass,
+            size=self.displayWidth,
+            extra=self.extra,)
+        if self.displayMaxWidth:
+            options.update(maxlength=self.displayMaxWidth)
+        if value:
+            filename = value.filename
+            if self.image_basename is not None:
+                filename = self.image_basename + os.path.splitext(filename)[-1]
+            result = renderElement(
+                'img',
+                src=filename,
+                )
+            result += renderElement('br')
+        result += renderElement(self.tag, **options)
+        if file_id is not None:
+            if value:
+                result += ' (%s)' % value.filename
+            result += renderElement(
+                'input',
+                type='hidden',
+                name=self.name + '.file_id',
+                id=self.name + '.file_id',
+                value=file_id,
+                )
+        return result
+
+    def _setFile(self, file):
+        """Store away uploaded file (FileUpload object).
+
+        Returns file_id identifying file.
+        """
+        # if there was no file input and there was a file_id already in the
+        # input, reuse this for next request
+        if not self.request.get(self.name):
+            file_id = self.request.get(self.name + '.file_id')
+            if file_id is not None:
+                return file_id
+        # otherwise, stuff filedata away in session, making a new file_id
+        if file == self.context.missing_value:
+            return None
+        return self._storeFile(file)
+
+    def _storeFile(self, file_upload):
+        # filenames are normally in unicode encoding, while the contents
+        # are byte streams. We turn the filename into a bytestream.
+        retrieval = queryUtility(IFileRetrieval)
+        if retrieval is not None:
+            file_upload.seek(0)
+            file_obj = retrieval.createFile(file_upload.filename, file_upload)
+            data = file_obj.data
+        else:
+            data = file_upload.read()
+        data = '%s\n%s' % (str(file_upload.filename), data)
+        return data.encode('base64')[:-1]
+
+    def _retrieveFile(self, file_id):
+        data = file_id.decode('base64')
+        filename, filedata = data.split('\n', 1)
+        return KofaImageFile(filename, filedata)
+
+class ThumbnailWidget(DownloadWidget):
+    """An image file widget that displays the data as thumbnail.
+
+    XXX: give some reason to name this a _thumbnail_ widget.
+    """
+
+    def __call__(self):
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+        if value == self.context.missing_value:
+            return renderElement(
+                u'div',
+                contents=u'Download not available')
+        filename = escape(value.filename)
+        return renderElement(
+            u'img',
+            src=filename,
+            contents=None)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/image.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/image.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/image.py	(revision 16045)
@@ -0,0 +1,49 @@
+## $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
+##
+"""
+Components for handling image files.
+"""
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import getUtility
+from zope.interface import implements
+from waeup.kofa.image.interfaces import IKofaImageFile
+
+class KofaImageFile(HurryFile):
+    """A file prepared for storing image files.
+
+    This file type is built upon :class:`hurry.file.HurryFile` but
+    implements a derived marker interface to distuingish it from
+    regular hurry files.
+
+    To create a :class:`KofaImageFile` you should use
+    :func:`createKofaImageFile`.
+    """
+    implements(IKofaImageFile)
+
+    def __ne__(self, other):
+        # This was wrongly implemented in the base class
+        try:
+            return (self.filename != other.filename or
+                    self.data != other.data)
+        except AttributeError:
+            return True
+
+def createKofaImageFile(filename, f):
+    retrieval = getUtility(IFileRetrieval)
+    return retrieval.createFile(filename, f)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/interfaces.py	(revision 16045)
@@ -0,0 +1,32 @@
+## $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
+##
+"""Interfaces for waeup.kofa.image
+"""
+from hurry.file.interfaces import IFile, IFileRetrieval, IHurryFile
+
+class IImageFile(IFile):
+    """Image file field.
+    """
+
+class IKofaImageFile(IHurryFile):
+    """Image file.
+    """
+
+class IImageFileRetrieval(IFileRetrieval):
+    """A file retrieval for images.
+    """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/schema.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/schema.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/schema.py	(revision 16045)
@@ -0,0 +1,84 @@
+## $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
+##
+"""Image file schemas.
+"""
+from zope.interface import implements
+from zope.schema import MinMaxLen
+from zope.schema.interfaces import TooBig, TooSmall
+from hurry.file.schema import File
+from waeup.kofa.image.interfaces import IImageFile
+from waeup.kofa.image.image import KofaImageFile
+
+class MinMaxSize(object):
+    """Expresses constraints on the size of an object.
+
+    The 'size' of an object is determined by its `size` method or
+    attribute. If an object has no such 'size' then it cannot be
+    validated by this mixin.
+
+    Please do not confuse `MinMaxSize` with `MinMaxLen`, for instance
+    supported by ordinary text fields. These test on ``len(obj)``
+    which is not necessary possible for file-like objects.
+
+    Therefore we distinguish 'size' from 'len' here.
+    """
+    min_size = None
+    max_size = None
+
+    def __init__(self, min_size=None, max_size=None, **kw):
+        self.min_size = min_size
+        self.max_size = max_size
+        super(MinMaxSize, self).__init__(**kw)
+
+    def _validate(self, value):
+        super(MinMaxSize, self)._validate(value)
+        if self.max_size is not None and value.size > self.max_size:
+            raise TooBig(
+                value.filename,
+                "%s bytes (max: %s bytes)" % (value.size, self.max_size))
+
+        if self.min_size is not None and value.size < self.min_size:
+            raise TooSmall(
+                value.filename,
+                "%s bytes (min: %s bytes)" % (value.size, self.min_size))
+
+    
+class ImageFile(MinMaxSize, File):
+    """An image file field.
+
+    Suitable for interfaces that wish to store image files in an
+    attribute.
+
+    This field type supports `MinMaxSize` so that you can set
+    `min_size` or `max_size` for all `ImageFile` fields in your
+    interfaces like this:
+
+      class MyInterface(Interface):
+         image = ImageFile(
+             title = u'The image',
+             description = u'The nice image',
+             max_size = 1024 * 10,
+             )
+
+    to restrict the file size of stored images to 10 KBytes.
+
+    By default no such restriction is set.
+    """
+    implements(IImageFile)
+
+    _type = KofaImageFile
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/tests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/tests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/image/tests.py	(revision 16045)
@@ -0,0 +1,37 @@
+## $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
+##
+import unittest
+
+from zope.testing import doctest
+from zope.app.testing import placelesssetup
+from zope import component
+
+from hurry.file.file import IdFileRetrieval
+from hurry.file.interfaces import IFileRetrieval
+
+def fileSetUp(doctest):
+    placelesssetup.setUp()
+    component.provideUtility(IdFileRetrieval(), IFileRetrieval)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'README.txt',
+            setUp=fileSetUp, tearDown=placelesssetup.tearDown,
+            ),
+        ))
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/imagestorage.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/imagestorage.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/imagestorage.py	(revision 16045)
@@ -0,0 +1,580 @@
+## $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
+##
+"""A storage for image (and other) files.
+
+A few words about storing files with ``waeup.kofa``. The need for this
+feature arised initially from the need to store passport files for
+applicants and students. These files are dynamic (can be changed
+anytime), mean a lot of traffic and cost a lot of memory/disk space.
+
+**Design Basics**
+
+While one *can* store images and similar 'large binary objects' aka
+blobs in the ZODB, this approach quickly becomes cumbersome and
+difficult to understand. The worst approach here would be to store
+images as regular byte-stream objects. ZODB supports this but
+obviously access is slow (data must be looked up in the one
+``Data.fs`` file, each file has to be sent to the ZEO server and back,
+etc.).
+
+A bit less worse is the approach to store images in the ZODB but as
+Blobs. ZODB supports storing blobs in separate files in order to
+accelerate lookup/retrieval of these files. The files, however, have
+to be sent to the ZEO server (and back on lookups) which means a
+bottleneck and will easily result in an increased number of
+``ConflictErrors`` even on simple reads.
+
+The advantage of both ZODB-geared approaches is, of course, complete
+database consistency. ZODB will guarantee that your files are
+available under some object name and can be handled as any other
+Python object.
+
+Another approach is to leave the ZODB behind and to store images and
+other files in filesystem directly. This is faster (no ZEO contacts,
+etc.), reduces probability of `ConflictErrors`, keeps the ZODB
+smaller, and enables direct access (over filesystem) to the
+files. Furthermore steps might be better understandable for
+third-party developers. We opted for this last option.
+
+**External File Store**
+
+Our implementation for storing-files-API is defined in
+:class:`ExtFileStore`. An instance of this file storage (which is also
+able to store non-image files) is available at runtime as a global
+utility implementing :class:`waeup.kofa.interfaces.IExtFileStore`.
+
+The main task of this central component is to maintain a filesystem
+root path for all files to be stored. It also provides methods to
+store/get files under certain file ids which identify certain files
+locally.
+
+So, to store a file away, you can do something like this:
+
+  >>> from StringIO import StringIO
+  >>> from zope.component import getUtility
+  >>> from waeup.kofa.interfaces import IExtFileStore
+  >>> store = getUtility(IExtFileStore)
+  >>> store.createFile('myfile.txt', StringIO('some file content'))
+
+All you need is a filename and the file-like object containing the
+real file data.
+
+This will store the file somewhere (you shouldn't make too much
+assumptions about the real filesystem path here).
+
+Later, we can get the file back like this:
+
+  >>> store.getFile('myfile')
+  <open file ...>
+
+Please note, that we ask for ``myfile`` instead of ``myfile.jpg`` as
+the file id should not make a difference for different filename
+extensions. The file id for ``sample.jpg`` thus could simply be
+``sample``.
+
+What we get back is a file or file-like object already opened for
+reading:
+
+  >>> store.getFile('myfile').read()
+  'some file content'
+
+**Handlers: Special Places for Special Files**
+
+The file store supports special handling for certain files. For
+example we want applicant images to be stored in a different directory
+than student images, etc. Because the file store cannot know all
+details about these special treatment of certain files, it looks up
+helpers (handlers) to provide the information it needs for really
+storing the files at the correct location.
+
+That a file stored in filestore needs special handling can be
+indicated by special filenames. These filenames start with a marker like
+this::
+
+  __<MARKER-STRING>__real-filename
+
+Please note the double underscores before and after the marker
+string. They indicate that all in between is a marker.
+
+If you store a file in file store with such a filename (we call this a
+`file_id` to distuingish it from real world filenames), the file store
+will look up a handler for ``<MARKER-STRING>`` and pass it the file to
+store. The handler then will return the internal path to store the
+file and possibly do additional things as well like validating the
+file or similar.
+
+Examples for such a file store handler can be found in the
+:mod:`waeup.kofa.applicants.applicant` module. Please see also the
+:class:`DefaultFileStoreHandler` class below for more details.
+
+The file store looks up handlers by utility lookups: it looks for a
+named utiliy providing
+:class:`waeup.kofa.interfaces.IFileStoreHandler` and named like the
+marker string (without leading/trailing underscores) in lower
+case. For example if the file id would be
+
+  ``__IMG_USER__manfred``
+
+then the looked up utility should be registered under name
+
+  ``img_user``
+
+and provide :class:`waeup.kofa.interfaces.IFileStoreHandler`. If no
+such utility can be found, a default handler is used instead
+(see :class:`DefaultFileStoreHandler`).
+
+**About File IDs and Filenames**
+
+In the waeup.kofa package we want to store documents like CVs,
+photographs, and similar. Each of this documents might come into the
+system with different filename extensions. This could be a problem as
+the browser components might have to set different response headers
+for different filetypes and we nevertheless want to make sure that
+only one file is stored per document. For instance we don't want
+``passport.jpg`` *and* ``passport.png`` but only one of them.
+
+The default components like :class:`DefaultFileStoreHandler` take care
+of this by searching the filesystem for already existing files with
+same file id and eventually removing them.
+
+Therefore file ids should never include filename extensions (except if
+you only support exactly one filename extension for a certain
+document). The only part where you should add an extension (and it is
+important to do so) is when creating new files: when a file was
+uploaded you can pass in the filename (including the filename
+extension) and the file stored in external file store will (most
+probably) have a different name but the same extension as the original
+file.
+
+When looking for the file, you however only have to give the file id
+and the handlers should find the right file for you, regardless of the
+filename extension it has.
+
+**Context Adapters: Knowing Your Family**
+
+Often the internal filename or file id of a file depends on a
+context. For example when we store passport photographs of applicants,
+then each image belongs to a certain applicant instance. It is not
+difficult to maintain such a connection manually: Say every applicant
+had an id, then we could put this id into the filename as well and
+would build the filename to store/get the connected file by using that
+filename. You then would create filenames of a format like this::
+
+  __<MARKER-STRING>__applicant0001
+
+where ``applicant0001`` would tell exactly which applicant you can see
+on the photograph. You notice that the internal file id might have
+nothing to do with once uploaded filenames. The id above could have
+been uploaded with filename ``manfred.jpg`` but with the new file id
+we are able to find the file again later.
+
+Unfortunately it might soon get boring or cumbersome to retype this
+building of filenames for a certain type of context, especially if
+your filenames take more of the context into account than only a
+simple id.
+
+Therefore you can define filename building for a context as an adapter
+that then could be looked up by other components simply by doing
+something like:
+
+  >>> from waeup.kofa.interfaces import IFileStoreNameChooser
+  >>> file_id = IFileStoreNameChooser(my_context_obj)
+
+If you later want to change the way file ids are created from a
+certain context, you only have to change the adapter implementation
+accordingly.
+
+Note, that this is only a convenience component. You don't have to
+define context adapters but it makes things easier for others if you
+do, as you don't have to remember the exact file id creation method
+all the time and can change things quick and in only one location if
+you need to do so.
+
+Please see the :class:`FileStoreNameChooser` default implementation
+below for details.
+
+"""
+import glob
+import grok
+import os
+import tempfile
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import queryUtility
+from zope.interface import Interface
+from waeup.kofa.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class FileStoreNameChooser(grok.Adapter):
+    """Default file store name chooser.
+
+    File store name choosers pick a file id, a string, for a certain
+    context object. They are normally registered as adapters for a
+    certain content type and know how to build the file id for this
+    special type of context.
+
+    Provides the :class:`waeup.kofa.interfaces.IFileStoreNameChooser`
+    interface.
+
+    This default file name chosser accepts almost every name as long
+    as it is a string or unicode object.
+    """
+    grok.context(Interface)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name, attr=None):
+        """Check whether a given name (file id) is valid.
+
+        Raises a user error if the name is not valid.
+
+        For the default file store name chooser any name is valid as
+        long as it is a string.
+
+        The `attr` is not taken into account here.
+        """
+        if isinstance(name, basestring):
+            return True
+        return False
+
+    def chooseName(self, name, attr=None):
+        """Choose a unique valid file id for the object.
+
+        The given name may be taken into account when choosing the
+        name (file id).
+
+        chooseName is expected to always choose a valid name (that
+        would pass the checkName test) and never raise an error.
+
+        For this default name chooser we return the given name if it
+        is valid or ``unknown_file`` else. The `attr` param is not
+        taken into account here.
+        """
+        if self.checkName(name):
+            return name
+        return u'unknown_file'
+
+class ExtFileStore(object):
+    """External file store.
+
+    External file stores are meant to store files 'externally' of the
+    ZODB, i.e. in filesystem.
+
+    Most important attribute of the external file store is the `root`
+    path which gives the path to the location where files will be
+    stored within.
+
+    By default `root` is a ``'media/'`` directory in the root of the
+    datacenter root of a site.
+
+    The `root` attribute is 'read-only' because you normally don't
+    want to change this path -- it is dynamic. That means, if you call
+    the file store from 'within' a site, the root path will be located
+    inside this site (a :class:`waeup.kofa.University` instance). If
+    you call it from 'outside' a site some temporary dir (always the
+    same during lifetime of the file store instance) will be used. The
+    term 'temporary' tells what you can expect from this path
+    persistence-wise.
+
+    If you insist, you can pass a root path on initialization to the
+    constructor but when calling from within a site afterwards, the
+    site will override your setting for security measures. This way
+    you can safely use one file store for different sites in a Zope
+    instance simultanously and files from one site won't show up in
+    another.
+
+    An ExtFileStore instance is available as a global utility
+    implementing :class:`waeup.kofa.interfaces.IExtFileStore`.
+
+    To add and retrieve files from the storage, use the appropriate
+    methods below.
+    """
+
+    grok.implements(IExtFileStore)
+
+    _root = None
+
+    @property
+    def root(self):
+        """Root dir of this storage.
+
+        The root dir is a readonly value determined dynamically. It
+        holds media files for sites or other components.
+
+        If a site is available we return a ``media/`` dir in the
+        datacenter storage dir.
+
+        Otherwise we create a temporary dir which will be remembered
+        on next call.
+
+        If a site exists and has a datacenter, it has always
+        precedence over temporary dirs, also after a temporary
+        directory was created.
+
+        Please note that retrieving `root` is expensive. You might
+        want to store a copy once retrieved in order to minimize the
+        number of calls to `root`.
+
+        """
+        site = grok.getSite()
+        if site is not None:
+            root = os.path.join(site['datacenter'].storage, 'media')
+            return root
+        if self._root is None:
+            self._root = tempfile.mkdtemp()
+        return self._root
+
+    def __init__(self, root=None):
+        self._root = root
+        return
+
+    def _pathFromFileID(self, file_id):
+        """Helper method to create filesystem path from FileID.
+
+        Used class-internally. Do not rely on this method when working
+        with an :class:`ExtFileStore` instance from other components.
+        """
+        marker, filename, base, ext = self.extractMarker(file_id)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        path = handler.pathFromFileID(self, self.root, file_id)
+        return path
+
+    def getFile(self, file_id):
+        """Get a file stored under file ID `file_id`.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This methods takes into account registered handlers for any
+        marker put into the file_id.
+
+        .. seealso:: :class:`DefaultFileStoreHandler`
+        """
+        path = self._pathFromFileID(file_id)
+        if not os.path.exists(path):
+            return None
+        fd = open(path, 'rb')
+        return fd
+
+    def getFileByContext(self, context, attr=None):
+        """Get a file for given context.
+
+        Returns a file already opened for reading.
+
+        If the file cannot be found ``None`` is returned.
+
+        This method takes into account registered handlers and file
+        name choosers for context types to build an intermediate file
+        id for the context and `attr` given.
+
+        Both, `context` and `attr` are used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method that internally calls
+        :meth:`getFile`.
+
+        .. seealso:: :class:`FileStoreNameChooser`,
+                     :class:`DefaultFileStoreHandler`.
+        """
+        file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+        return self.getFile(file_id)
+
+    def deleteFile(self, file_id):
+        """Delete file stored under `file_id` in storage.
+
+        The file is physically removed from filesystem.
+        """
+        path = self._pathFromFileID(file_id)
+        if not os.path.exists(path) or not os.path.isfile(path):
+            return
+        os.unlink(path)
+        return
+
+    def deleteFileByContext(self, context, attr=None):
+        """Remove file identified by `context` and `attr` if it exists.
+
+        This method takes into account registered handlers and file
+        name choosers for context types to build an intermediate file
+        id for the context and `attr` given.
+
+        Both, `context` and `attr` are used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method that internally calls
+        :meth:`getFile`.
+
+        .. seealso:: :class:`FileStoreNameChooser`,
+                     :class:`DefaultFileStoreHandler`.
+
+        """
+        file_id = IFileStoreNameChooser(context).chooseName(attr=attr)
+        return self.deleteFile(file_id)
+
+    def createFile(self, filename, f):
+        """Store a file.
+        """
+        root = self.root # Calls to self.root are expensive
+        file_id = os.path.splitext(filename)[0]
+        marker, filename, base, ext = self.extractMarker(filename)
+        handler = queryUtility(IFileStoreHandler, name=marker,
+                               default=DefaultFileStoreHandler())
+        f, path, file_obj = handler.createFile(
+            self, root, filename, file_id, f)
+        dirname = os.path.dirname(path)
+        if not os.path.exists(dirname):
+            os.makedirs(dirname, 0755)
+        open(path, 'wb').write(f.read())
+        return file_obj
+
+    def extractMarker(self, file_id):
+        """split filename into marker, filename, basename, and extension.
+
+        A marker is a leading part of a string of form
+        ``__MARKERNAME__`` followed by the real filename. This way we
+        can put markers into a filename to request special processing.
+
+        Returns a quadruple
+
+          ``(marker, filename, basename, extension)``
+
+        where ``marker`` is the marker in lowercase, filename is the
+        complete trailing real filename, ``basename`` is the basename
+        of the filename and ``extension`` the filename extension of
+        the trailing filename. See examples below.
+
+        Example:
+
+           >>> extractMarker('__MaRkEr__sample.jpg')
+           ('marker', 'sample.jpg', 'sample', '.jpg')
+
+        If no marker is contained, we assume the whole string to be a
+        real filename:
+
+           >>> extractMarker('no-marker.txt')
+           ('', 'no-marker.txt', 'no-marker', '.txt')
+
+        Filenames without extension give an empty extension string:
+
+           >>> extractMarker('no-marker')
+           ('', 'no-marker', 'no-marker', '')
+
+        """
+        if not isinstance(file_id, basestring) or not file_id:
+            return ('', '', '', '')
+        parts = file_id.split('__', 2)
+        marker = ''
+        if len(parts) == 3 and parts[0] == '':
+            marker = parts[1].lower()
+            file_id = parts[2]
+        basename, ext = os.path.splitext(file_id)
+        return (marker, file_id, basename, ext)
+
+grok.global_utility(ExtFileStore, provides=IExtFileStore)
+
+class DefaultStorage(ExtFileStore):
+    """Default storage for files.
+
+    Registered globally as utility for
+    :class:`hurry.file.interfaces.IFileRetrieval`.
+    """
+    grok.provides(IFileRetrieval)
+
+grok.global_utility(DefaultStorage, provides=IFileRetrieval)
+
+class DefaultFileStoreHandler(grok.GlobalUtility):
+    """A default handler for external file store.
+
+    This handler is the fallback called by external file stores when
+    there is no or an unknown marker in the file id.
+
+    Registered globally as utility for
+    :class:`waeup.kofa.interfaces.IFileStoreHandler`.
+    """
+    grok.implements(IFileStoreHandler)
+
+    def _searchInPath(self, path):
+        """Get complete path of any existing file starting with `path`.
+
+        If no such file can be found, return input path.
+
+        If multiple such files exist, return the first one.
+
+        **Example:**
+
+        Looking for a `path`::
+
+          '/tmp/myfile'
+
+        will find any file like ``'/tmp/myfile.txt'``,
+        ``'/tmp/myfile.jpg'`` but also ``'/tmp/myfile_any_attribute.pdf'``,
+        if it exists. Therefore we must be careful. File attributes
+        must come first: ``'/tmp/any_attribute_myfile.pdf'``
+        """
+        result = path
+        if os.path.isdir(os.path.dirname(path)):
+            file_iter = glob.iglob('%s*' % (path,))
+            try:
+                result = file_iter.next()
+            except StopIteration:
+                pass
+        return result
+
+    def pathFromFileID(self, store, root, file_id):
+        """Return a path for getting/storing a file with given file id.
+
+        If there is already a file stored for the given file id, the
+        path to this file is returned.
+
+        If no such file exists yet (or the the only file existing has
+        no filename extension at all) a path to store the file but
+        without any filename extension is returned.
+        """
+        path = os.path.join(root, file_id)
+        return self._searchInPath(path)
+
+    def createFile(self, store, root, filename, file_id, f):
+        """Infos about what to store exactly and where.
+
+        When a file should be handled by an external file storage, it
+        looks up any handlers (like this one), passes runtime infos
+        like the storage object, root path, filename, file_id, and the
+        raw file object itself.
+
+        The handler can then change the file, raise exceptions or
+        whatever and return the result.
+
+        This handler returns the input file as-is, a path returned by
+        :meth:`pathFromFileID` and an instance of
+        :class:`hurry.file.HurryFile` for further operations.
+
+        Please note: although a handler has enough infos to store the
+        file itself, it should leave that task to the calling file
+        store.
+
+        This method does, however, remove any existing files stored
+        under the given file id.
+        """
+        ext = os.path.splitext(filename)[1]
+        path = self.pathFromFileID(store, root, file_id)
+        base, old_ext = os.path.splitext(path)
+        if old_ext != ext:
+            if os.path.exists(path):
+                os.unlink(path)
+            path = base + ext
+        return f, path, HurryFile(filename, file_id + ext)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/interfaces.py	(revision 16045)
@@ -0,0 +1,1551 @@
+## $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
+##
+import os
+import re
+import codecs
+import zc.async.interfaces
+import zope.i18nmessageid
+from datetime import datetime
+from hurry.file.interfaces import IFileRetrieval
+from hurry.workflow.interfaces import IWorkflowInfo
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope import schema
+from zope.pluggableauth.interfaces import IPrincipalInfo
+from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
+from zope.component import getUtility
+from zope.component.interfaces import IObjectEvent
+from zope.configuration.fields import Path
+from zope.container.interfaces import INameChooser, IContainer
+from zope.interface import Interface, Attribute
+from zope.schema.interfaces import IObject
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+from waeup.kofa.schema import PhoneNumber
+from waeup.kofa.sourcefactory import SmartBasicContextualSourceFactory
+
+_ = MessageFactory = zope.i18nmessageid.MessageFactory('waeup.kofa')
+
+DELETION_MARKER = 'XXX'
+IGNORE_MARKER = '<IGNORE>'
+WAEUP_KEY = 'waeup.kofa'
+VIRT_JOBS_CONTAINER_NAME = 'jobs'
+DOCLINK = 'http://kofa-doc.waeup.org/userdocs'
+
+CREATED = 'created'
+ADMITTED = 'admitted'
+CLEARANCE = 'clearance started'
+REQUESTED = 'clearance requested'
+CLEARED = 'cleared'
+PAID = 'school fee paid'
+RETURNING = 'returning'
+REGISTERED = 'courses registered'
+VALIDATED = 'courses validated'
+GRADUATED = 'graduated'
+TRANSREQ = 'transcript requested'
+TRANSVAL = 'transcript validated'
+TRANSREL = 'transcript released'
+
+
+#: A dict giving job status as tuple (<STRING>, <TRANSLATED_STRING>),
+#: the latter for UI purposes.
+JOB_STATUS_MAP = {
+    zc.async.interfaces.NEW: ('new', _('new')),
+    zc.async.interfaces.COMPLETED: ('completed', _('completed')),
+    zc.async.interfaces.PENDING: ('pending', _('pending')),
+    zc.async.interfaces.ACTIVE: ('active', _('active')),
+    zc.async.interfaces.ASSIGNED: ('assigned', _('assigned')),
+    zc.async.interfaces.CALLBACKS: ('callbacks', _('callbacks')),
+    }
+
+#default_rest_frontpage = u'' + codecs.open(os.path.join(
+#        os.path.dirname(__file__), 'frontpage.rst'),
+#        encoding='utf-8', mode='rb').read()
+
+default_html_frontpage = u'' + codecs.open(os.path.join(
+        os.path.dirname(__file__), 'frontpage.html'),
+        encoding='utf-8', mode='rb').read()
+
+def SimpleKofaVocabulary(*terms):
+    """A well-buildt vocabulary provides terms with a value, token and
+       title for each term
+    """
+    return SimpleVocabulary([
+            SimpleTerm(value, value, title) for title, value in terms])
+
+def academic_sessions():
+    curr_year = datetime.now().year
+    year_range = range(1980, curr_year + 2)
+    return [('%s/%s' % (year,year+1), year) for year in year_range]
+
+academic_sessions_vocab = SimpleKofaVocabulary(*academic_sessions())
+
+registration_states_vocab = SimpleKofaVocabulary(
+    (_('created'), CREATED),
+    (_('admitted'), ADMITTED),
+    (_('clearance started'), CLEARANCE),
+    (_('clearance requested'), REQUESTED),
+    (_('cleared'), CLEARED),
+    (_('school fee paid'), PAID),
+    (_('courses registered'), REGISTERED),
+    (_('courses validated'), VALIDATED),
+    (_('returning'), RETURNING),
+    (_('graduated'), GRADUATED),
+    (_('transcript requested'), TRANSREQ),
+    (_('transcript validated'), TRANSVAL),
+    (_('transcript released'), TRANSREL),
+    )
+
+class ContextualDictSourceFactoryBase(SmartBasicContextualSourceFactory):
+    """A base for contextual sources based on KofaUtils dicts.
+
+    To create a real source, you have to set the `DICT_NAME` attribute
+    which should be the name of a dictionary in KofaUtils.
+    """
+    def getValues(self, context):
+        utils = getUtility(IKofaUtils)
+        sorted_items = sorted(getattr(utils, self.DICT_NAME).items(),
+                              key=lambda item: item[1])
+        return [item[0] for item in sorted_items]
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        utils = getUtility(IKofaUtils)
+        return getattr(utils, self.DICT_NAME)[value]
+
+class SubjectSource(BasicSourceFactory):
+    """A source for school subjects used in exam documentation.
+    """
+    def getValues(self):
+        subjects_dict = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
+        return sorted(subjects_dict.keys())
+
+    def getTitle(self, value):
+        subjects_dict = getUtility(IKofaUtils).EXAM_SUBJECTS_DICT
+        return "%s:" % subjects_dict[value]
+
+class GradeSource(BasicSourceFactory):
+    """A source for exam grades.
+    """
+    def getValues(self):
+        for entry in getUtility(IKofaUtils).EXAM_GRADES:
+            yield entry[0]
+
+    def getTitle(self, value):
+        return dict(getUtility(IKofaUtils).EXAM_GRADES)[value]
+
+class DisablePaymentGroupSource(ContextualDictSourceFactoryBase):
+    """A source for filtering groups of students
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'DISABLE_PAYMENT_GROUP_DICT'
+
+# Define a validation method for email addresses
+class NotAnEmailAddress(schema.ValidationError):
+    __doc__ = u"Invalid email address"
+
+#: Regular expression to check email-address formats. As these can
+#: become rather complex (nearly everything is allowed by RFCs), we only
+#: forbid whitespaces, commas and dots following onto each other.
+check_email = re.compile(
+    r"^[^@\s,]+@[^@\.\s,]+(\.[^@\.\s,]+)*$").match
+
+def validate_email(value):
+    if not check_email(value):
+        raise NotAnEmailAddress(value)
+    return True
+
+# Define a validation method for ids
+class NotIdValue(schema.ValidationError):
+    __doc__ = u"Invalid id"
+
+#: Regular expressions to check id formats.
+check_id = re.compile(r"^[a-zA-Z0-9_-]{2,11}$").match
+
+def validate_id(value):
+    if not check_id(value):
+        raise NotIdValue(value)
+    return True
+
+# Define a validation method for HTML fields
+class NotHTMLValue(schema.ValidationError):
+    __doc__ = u"Style or script elements not allowed"
+
+def validate_html(value):
+    if '<style' in value or '<script' in value:
+        raise NotHTMLValue(value)
+    return True
+
+# Define a validation method for international phone numbers
+class InvalidPhoneNumber(schema.ValidationError):
+    __doc__ = u"Invalid phone number"
+
+# represent format +NNN-NNNN-NNNN
+RE_INT_PHONE = re.compile(r"^\+?\d+\-\d+\-[\d\-]+$")
+
+def validate_phone(value):
+    if not RE_INT_PHONE.match(value):
+        raise InvalidPhoneNumber(value)
+    return True
+
+class FatalCSVError(Exception):
+    """Some row could not be processed.
+    """
+    pass
+
+class DuplicationError(Exception):
+    """An exception that can be raised when duplicates are found.
+
+    When raising :exc:`DuplicationError` you can, beside the usual
+    message, specify a list of objects which are duplicates. These
+    values can be used by catching code to print something helpful or
+    similar.
+    """
+    def __init__(self, msg, entries=[]):
+        self.msg = msg
+        self.entries = entries
+
+    def __str__(self):
+        return '%r' % self.msg
+
+class RoleSource(BasicSourceFactory):
+    """A source for site roles.
+    """
+    def getValues(self):
+        # late import: in interfaces we should not import local modules
+        from waeup.kofa.permissions import get_waeup_role_names
+        return get_waeup_role_names()
+
+    def getTitle(self, value):
+        # late import: in interfaces we should not import local modules
+        from waeup.kofa.permissions import get_all_roles
+        roles = dict(get_all_roles())
+        if value in roles.keys():
+            title = roles[value].title
+            if '.' in title:
+                title = title.split('.', 2)[1]
+        return title
+
+class CaptchaSource(BasicSourceFactory):
+    """A source for captchas.
+    """
+    def getValues(self):
+        captchas = ['No captcha', 'Testing captcha', 'ReCaptcha']
+        try:
+            # we have to 'try' because IConfiguration can only handle
+            # interfaces from w.k.interface.
+            from waeup.kofa.browser.interfaces import ICaptchaManager
+        except:
+            return captchas
+        return sorted(getUtility(ICaptchaManager).getAvailCaptchas().keys())
+
+    def getTitle(self, value):
+        return value
+
+class IResultEntry(Interface):
+    """A school grade entry.
+    """
+
+    subject = schema.Choice(
+        title = _(u'Subject'),
+        source = SubjectSource(),
+        )
+
+    grade = schema.Choice(
+        title = _(u'Grade'),
+        source = GradeSource(),
+        )
+
+class IResultEntryField(IObject):
+    """A zope.schema-like field for usage in interfaces.
+
+    Marker interface to distuingish result entries from ordinary
+    object fields. Needed for registration of widgets.
+    """
+
+class IRefereeEntry(Interface):
+    """A referee entry.
+    """
+    email_sent = Attribute('True if email has been sent')
+
+    name = schema.TextLine(
+        title = _(u'Name'),
+        required = True,
+        description = _(u'Name'),
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        description = _(
+            u'Email Address (referees will be automatically invited by email '
+             'after final submission of this form)'),
+        )
+
+class IRefereeEntryField(IObject):
+    """A zope.schema-like field for usage in interfaces.
+
+    Marker interface to distuingish referee entries from ordinary
+    object fields. Needed for registration of widgets.
+    """
+
+class IKofaUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+
+    PORTAL_LANGUAGE = Attribute("Dict of global language setting")
+    PREFERRED_LANGUAGES_DICT = Attribute("Dict of preferred languages")
+    EXAM_SUBJECTS_DICT = Attribute("Dict of examination subjects")
+    EXAM_GRADES = Attribute("Dict of examination grades")
+    INST_TYPES_DICT = Attribute("Dict if institution types")
+    STUDY_MODES_DICT = Attribute("Dict of study modes")
+    APP_CATS_DICT = Attribute("Dict of application categories")
+    SEMESTER_DICT = Attribute("Dict of semesters or trimesters")
+    SYSTEM_MAX_LOAD = Attribute("Dict of maximum system loads.")
+    TEMP_PASSWORD_MINUTES = Attribute(
+        "Temporary passwords and parents password validity period")
+
+    def sendContactForm(
+          from_name,from_addr,rcpt_name,rcpt_addr,
+          from_username,usertype,portal,body,subject):
+        """Send an email with data provided by forms.
+        """
+
+    def fullname(firstname,lastname,middlename):
+        """Full name constructor.
+        """
+
+    def sendCredentials(user, password, url_info, msg):
+        """Send credentials as email.
+
+        Input is the applicant for which credentials are sent and the
+        password.
+
+        Returns True or False to indicate successful operation.
+        """
+
+    def genPassword(length, chars):
+        """Generate a random password.
+        """
+
+class IKofaObject(Interface):
+    """A Kofa object.
+
+    This is merely a marker interface.
+    """
+
+class IUniversity(IKofaObject):
+    """Representation of a university.
+    """
+
+
+class IKofaContainer(IKofaObject):
+    """A container for Kofa objects.
+    """
+
+class IKofaContained(IKofaObject):
+    """An item contained in an IKofaContainer.
+    """
+
+class ICSVExporter(Interface):
+    """A CSV file exporter for objects.
+    """
+    fields = Attribute("""List of fieldnames in resulting CSV""")
+
+    title = schema.TextLine(
+        title = u'Title',
+        description = u'Description to be displayed in selections.',
+        )
+    def mangle_value(value, name, obj):
+        """Mangle `value` extracted from `obj` or suobjects thereof.
+
+        This is called by export before actually writing to the result
+        file.
+        """
+
+    def get_filtered(site, **kw):
+        """Get datasets in `site` to be exported.
+
+        The set of data is specified by keywords, which might be
+        different for any implementaion of exporter.
+
+        Returns an iterable.
+        """
+
+    def get_selected(site, selected):
+        """Get datasets in `site` to be exported.
+
+        The set of data is specified by a list of identifiers.
+
+        Returns an iterable.
+        """
+
+    def export(iterable, filepath=None):
+        """Export iterables as rows in a CSV file.
+
+        If `filepath` is not given, a string with the data should be
+        returned.
+
+        What kind of iterables are acceptable depends on the specific
+        exporter implementation.
+        """
+
+    def export_all(site, filepath=None):
+        """Export all items in `site` as CSV file.
+
+        if `filepath` is not given, a string with the data should be
+        returned.
+        """
+
+    def export_filtered(site, filepath=None, **kw):
+        """Export those items in `site` specified by `args` and `kw`.
+
+        If `filepath` is not given, a string with the data should be
+        returned.
+
+        Which special keywords are supported is up to the respective
+        exporter.
+        """
+
+    def export_selected(site, filepath=None, **kw):
+        """Export items in `site` specified by a list of identifiers
+        called `selected`.
+
+        If `filepath` is not given, a string with the data should be
+        returned.
+        """
+
+class IKofaExporter(Interface):
+    """An exporter for objects.
+    """
+    def export(obj, filepath=None):
+        """Export by pickling.
+
+        Returns a file-like object containing a representation of `obj`.
+
+        This is done using `pickle`. If `filepath` is ``None``, a
+        `cStringIO` object is returned, that contains the saved data.
+        """
+
+class IKofaXMLExporter(Interface):
+    """An XML exporter for objects.
+    """
+    def export(obj, filepath=None):
+        """Export as XML.
+
+        Returns an XML representation of `obj`.
+
+        If `filepath` is ``None``, a StringIO` object is returned,
+        that contains the transformed data.
+        """
+
+class IKofaXMLImporter(Interface):
+    """An XML import for objects.
+    """
+    def doImport(filepath):
+        """Create Python object from XML.
+
+        Returns a Python object.
+        """
+
+class IBatchProcessor(Interface):
+    """A batch processor that handles mass-operations.
+    """
+    name = schema.TextLine(
+        title = _(u'Processor name')
+        )
+
+    def doImport(path, headerfields, mode='create', user='Unknown',
+                 logger=None, ignore_empty=True):
+        """Read data from ``path`` and update connected object.
+
+        `headerfields` is a list of headerfields as read from the file
+        to import.
+
+        `mode` gives the import mode to use (``'create'``,
+        ``'update'``, or ``'remove'``.
+
+        `user` is a string describing the user performing the
+        import. Normally fetched from current principal.
+
+        `logger` is the logger to use during import.
+
+        `ignore_emtpy` in update mode ignores empty fields if true.
+        """
+
+class IContactForm(IKofaObject):
+    """A contact form.
+    """
+
+    email_from = schema.ASCIILine(
+        title = _(u'Email Address:'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    email_to = schema.ASCIILine(
+        title = _(u'Email to:'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    subject = schema.TextLine(
+        title = _(u'Subject:'),
+        required = True,)
+
+    fullname = schema.TextLine(
+        title = _(u'Full Name:'),
+        required = True,)
+
+    body = schema.Text(
+        title = _(u'Text:'),
+        required = True,)
+
+class IKofaPrincipalInfo(IPrincipalInfo):
+    """Infos about principals that are users of Kofa Kofa.
+    """
+    email = Attribute("The email address of a user")
+    phone = Attribute("The phone number of a user")
+    public_name = Attribute("The public name of a user")
+    user_type = Attribute("The type of a user")
+
+
+class IKofaPrincipal(IPrincipal):
+    """A principle for Kofa Kofa.
+
+    This interface extends zope.security.interfaces.IPrincipal and
+    requires also an `id` and other attributes defined there.
+    """
+
+    email = schema.TextLine(
+        title = _(u'Email Address'),
+        description = u'',
+        required=False,)
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        description = u'',
+        required=False,)
+
+    public_name = schema.TextLine(
+        title = _(u'Public Name'),
+        required = False,)
+
+    user_type = Attribute('The user type of the principal')
+
+class IFailedLoginInfo(IKofaObject):
+    """Info about failed logins.
+
+    Timestamps are supposed to be stored as floats using time.time()
+    or similar.
+    """
+    num = schema.Int(
+        title = _(u'Number of failed logins'),
+        description = _(u'Number of failed logins'),
+        required = True,
+        default = 0,
+        )
+
+    last = schema.Float(
+        title = _(u'Timestamp'),
+        description = _(u'Timestamp of last failed login or `None`'),
+        required = False,
+        default = None,
+        )
+
+    def as_tuple():
+        """Get login info as tuple ``<NUM>, <TIMESTAMP>``.
+        """
+
+    def set_values(num=0, last=None):
+        """Set number of failed logins and timestamp of last one.
+        """
+
+    def increase():
+        """Increase the current number of failed logins and set timestamp.
+        """
+
+    def reset():
+        """Set failed login counters back to zero.
+        """
+
+
+class IUserAccount(IKofaObject):
+    """A user account.
+    """
+
+    failed_logins = Attribute('FailedLoginInfo for this account')
+
+    name = schema.TextLine(
+        title = _(u'User Id'),
+        description = _(u'Login name of user'),
+        required = True,)
+
+    title = schema.TextLine(
+        title = _(u'Full Name'),
+        required = True,)
+
+    public_name = schema.TextLine(
+        title = _(u'Public Name'),
+        description = _(u"Substitute for officer's real name "
+                       "in student object histories."),
+        required = False,)
+
+    description = schema.Text(
+        title = _(u'Description/Notice'),
+        required = False,)
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        default = None,
+        required = False,
+        )
+
+    roles = schema.List(
+        title = _(u'Portal Roles'),
+        value_type = schema.Choice(source=RoleSource()),
+        required = False,
+        )
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        description = _(u'If set, the account is immediately blocked.'),
+        default = False,
+        required = False,
+        )
+
+
+class IPasswordValidator(Interface):
+    """A password validator utility.
+    """
+
+    def validate_password(password, password_repeat):
+        """Validates a password by comparing it with
+        control password and checking some other requirements.
+        """
+
+    def validate_secure_password(self, pw, pw_repeat):
+        """ Validates a password by comparing it with
+        control password and checks password strength by
+        matching with the regular expression:
+
+        ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$
+
+        ^              Start anchor
+        (?=.*[A-Z])    Ensure password has one uppercase letters.
+        (?=.*[0-9])    Ensure password has one digit.
+        (?=.*[a-z])    Ensure password has one lowercase letter.
+        .{8,}          Ensure password is of length 8.
+        $              End anchor.
+        """
+
+
+class IUsersContainer(IKofaObject):
+    """A container for officers.
+    """
+
+    def addUser(name, password, title=None, description=None):
+        """Add a user.
+        """
+
+    def delUser(name):
+        """Delete a user if it exists.
+        """
+
+class ILocalRolesAssignable(Interface):
+    """The local roles assignable to an object.
+    """
+    def __call__():
+        """Returns a list of dicts.
+
+        Each dict contains a ``name`` referring to the role assignable
+        for the specified object and a `title` to describe the range
+        of users to which this role can be assigned.
+        """
+
+class IConfigurationContainer(IKofaObject):
+    """A container for session configuration objects.
+    """
+
+    name = schema.TextLine(
+        title = _(u'Name of University'),
+        default = _(u'Sample University'),
+        required = True,
+        )
+
+    acronym = schema.TextLine(
+        title = _(u'Abbreviated Title of University'),
+        default = u'WAeUP.Kofa',
+        required = True,
+        )
+
+    frontpage = schema.Text(
+        title = _(u'Content in HTML format'),
+        required = False,
+        default = default_html_frontpage,
+        constraint=validate_html,
+        )
+
+    frontpage_dict = schema.Dict(
+        title = u'Content as language dictionary with values in html format',
+        required = False,
+        default = {},
+        )
+
+    name_admin = schema.TextLine(
+        title = _(u'Name of Administrator'),
+        default = u'Administrator',
+        required = True,
+        )
+
+    email_admin = schema.ASCIILine(
+        title = _(u'Email Address of Administrator'),
+        default = 'contact@waeup.org',
+        required = True,
+        #constraint=validate_email,
+        )
+
+    email_subject = schema.TextLine(
+        title = _(u'Subject of Email to Administrator'),
+        default = _(u'Kofa Contact'),
+        required = True,
+        )
+
+    smtp_mailer = schema.Choice(
+        title = _(u'SMTP mailer to use when sending mail'),
+        vocabulary = 'Mail Delivery Names',
+        default = 'No email service',
+        required = True,
+        )
+
+    captcha = schema.Choice(
+        title = _(u'Captcha used for public registration pages'),
+        source = CaptchaSource(),
+        default = u'No captcha',
+        required = True,
+        )
+
+    carry_over = schema.Bool(
+        title = _(u'Carry-over Course Registration'),
+        default = False,
+        )
+
+    current_academic_session = schema.Choice(
+        title = _(u'Current Academic Session'),
+        description = _(u'Session for which score editing is allowed'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    next_matric_integer = schema.Int(
+        title = _(u'Next Matriculation Number Integer'),
+        description = _(u'Integer used for constructing the next '
+                         'matriculation number'),
+        default = 0,
+        readonly = False,
+        required = False,
+        )
+
+    next_matric_integer_2 = schema.Int(
+        title = _(u'Next Matriculation Number Integer 2'),
+        description = _(u'2nd integer used for constructing the next '
+                         'matriculation number'),
+        default = 0,
+        readonly = False,
+        required = False,
+        )
+
+    next_matric_integer_3 = schema.Int(
+        title = _(u'Next Matriculation Number Integer 3'),
+        description = _(u'3rd integer used for constructing the next '
+                         'matriculation number'),
+        default = 0,
+        readonly = False,
+        required = False,
+        )
+
+    next_matric_integer_4 = schema.Int(
+        title = _(u'Next Matriculation Number Integer 4'),
+        description = _(u'4th integer used for constructing the next '
+                         'matriculation number'),
+        default = 0,
+        readonly = False,
+        required = False,
+        )
+
+    export_disabled_message = schema.Text(
+        title = _(u'Export-disabled message'),
+        description = _(u'Message which will show up if an officer tries '
+                         'to export data. All exporters are automatcally '
+                         'disabled if this field is set.'),
+        required = False,
+        )
+
+    maintmode_enabled_by = schema.TextLine(
+        title = _(u'Maintenance Mode enabled by'),
+        default = None,
+        required = False,
+        )
+
+    def addSessionConfiguration(sessionconfiguration):
+        """Add a session configuration object.
+        """
+
+class ISessionConfiguration(IKofaObject):
+    """A session configuration object.
+    """
+
+    academic_session = schema.Choice(
+        title = _(u'Academic Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        readonly = True,
+        )
+
+    clearance_enabled = schema.Bool(
+        title = _(u'Clearance enabled'),
+        default = False,
+        )
+
+    payment_disabled = schema.List(
+        title = _(u'Payment disabled'),
+        value_type = schema.Choice(
+            source = DisablePaymentGroupSource(),
+            ),
+        required = False,
+        defaultFactory=list,
+        )
+
+    coursereg_deadline = schema.Datetime(
+        title = _(u'Course Reg. Deadline'),
+        required = False,
+        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
+        )
+
+    clearance_fee = schema.Float(
+        title = _(u'Acceptance Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    booking_fee = schema.Float(
+        title = _(u'Bed Booking Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    maint_fee = schema.Float(
+        title = _(u'Rent (fallback)'),
+        default = 0.0,
+        required = False,
+        )
+
+    late_registration_fee = schema.Float(
+        title = _(u'Late Course Reg. Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    transcript_fee = schema.Float(
+        title = _(u'Transcript Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    transfer_fee = schema.Float(
+        title = _(u'Transfer Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    def getSessionString():
+        """Return the session string from the vocabulary.
+        """
+
+
+class ISessionConfigurationAdd(ISessionConfiguration):
+    """A session configuration object in add mode.
+    """
+
+    academic_session = schema.Choice(
+        title = _(u'Academic Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        readonly = False,
+        )
+
+ISessionConfigurationAdd['academic_session'].order =  ISessionConfiguration[
+    'academic_session'].order
+
+class IDataCenter(IKofaObject):
+    """A data center.
+
+    A data center manages files (uploads, downloads, etc.).
+
+    Beside providing the bare paths needed to keep files, it also
+    provides some helpers to put results of batch processing into
+    well-defined final locations (with well-defined filenames).
+
+    The main use-case is managing of site-related files, i.e. files
+    for import, export etc.
+
+    DataCenters are _not_ meant as storages for object-specific files
+    like passport photographs and similar.
+
+    It is up to the datacenter implementation how to organize data
+    (paths) inside its storage path.
+    """
+    storage = schema.Bytes(
+        title = u'Path to directory where everything is kept.'
+        )
+
+    deleted_path = schema.Bytes(
+        title = u'Path for deleted student data.'
+        )
+
+    graduated_path = schema.Bytes(
+        title = u'Path for deleted graduated student data.'
+        )
+
+    def getPendingFiles(sort='name'):
+        """Get a list of files stored in `storage` sorted by basename.
+        """
+
+    def getFinishedFiles():
+        """Get a list of files stored in `finished` subfolder of `storage`.
+        """
+
+    def setStoragePath(path, move=False, overwrite=False):
+        """Set the path where to store files.
+
+        If `move` is True, move over files from the current location
+        to the new one.
+
+        If `overwrite` is also True, overwrite any already existing
+        files of same name in target location.
+
+        Triggers a DataCenterStorageMovedEvent.
+        """
+
+    def distProcessedFiles(successful, source_path, finished_file,
+                           pending_file, mode='create', move_orig=True):
+        """Distribute processed files over final locations.
+        """
+
+
+class IDataCenterFile(Interface):
+    """A data center file.
+    """
+
+    name = schema.TextLine(
+        title = u'Filename')
+
+    size = schema.TextLine(
+        title = u'Human readable file size')
+
+    uploaddate = schema.TextLine(
+        title = u'Human readable upload datetime')
+
+    lines = schema.Int(
+        title = u'Number of lines in file')
+
+    def getDate():
+        """Get creation timestamp from file in human readable form.
+        """
+
+    def getSize():
+        """Get human readable size of file.
+        """
+
+    def getLinesNumber():
+        """Get number of lines of file.
+        """
+
+class IDataCenterStorageMovedEvent(IObjectEvent):
+    """Emitted, when the storage of a datacenter changes.
+    """
+
+class IObjectUpgradeEvent(IObjectEvent):
+    """Can be fired, when an object shall be upgraded.
+    """
+
+class ILocalRoleSetEvent(IObjectEvent):
+    """A local role was granted/revoked for a principal on an object.
+    """
+    role_id = Attribute(
+        "The role id that was set.")
+    principal_id = Attribute(
+        "The principal id for which the role was granted/revoked.")
+    granted = Attribute(
+        "Boolean. If false, then the role was revoked.")
+
+class IQueryResultItem(Interface):
+    """An item in a search result.
+    """
+    url = schema.TextLine(
+        title = u'URL that links to the found item')
+    title = schema.TextLine(
+        title = u'Title displayed in search results.')
+    description = schema.Text(
+        title = u'Longer description of the item found.')
+
+class IKofaPluggable(Interface):
+    """A component that might be plugged into a Kofa Kofa app.
+
+    Components implementing this interface are referred to as
+    'plugins'. They are normally called when a new
+    :class:`waeup.kofa.app.University` instance is created.
+
+    Plugins can setup and update parts of the central site without the
+    site object (normally a :class:`waeup.kofa.app.University` object)
+    needing to know about that parts. The site simply collects all
+    available plugins, calls them and the plugins care for their
+    respective subarea like the applicants area or the datacenter
+    area.
+
+    Currently we have no mechanism to define an order of plugins. A
+    plugin should therefore make no assumptions about the state of the
+    site or other plugins being run before and instead do appropriate
+    checks if necessary.
+
+    Updates can be triggered for instance by the respective form in
+    the site configuration. You normally do updates when the
+    underlying software changed.
+    """
+    def setup(site, name, logger):
+        """Create an instance of the plugin.
+
+        The method is meant to be called by the central app (site)
+        when it is created.
+
+        `site`:
+           The site that requests a setup.
+
+        `name`:
+           The name under which the plugin was registered (utility name).
+
+        `logger`:
+           A standard Python logger for the plugins use.
+        """
+
+    def update(site, name, logger):
+        """Method to update an already existing plugin.
+
+        This might be called by a site when something serious
+        changes. It is a poor-man replacement for Zope generations
+        (but probably more comprehensive and better understandable).
+
+        `site`:
+           The site that requests an update.
+
+        `name`:
+           The name under which the plugin was registered (utility name).
+
+        `logger`:
+           A standard Python logger for the plugins use.
+        """
+
+class IAuthPluginUtility(Interface):
+    """A component that cares for authentication setup at site creation.
+
+    Utilities providing this interface are looked up when a Pluggable
+    Authentication Utility (PAU) for any
+    :class:`waeup.kofa.app.University` instance is created and put
+    into ZODB.
+
+    The setup-code then calls the `register` method of the utility and
+    expects a modified (or unmodified) version of the PAU back.
+
+    This allows to define any authentication setup modifications by
+    submodules or third-party modules/packages.
+    """
+
+    def register(pau):
+        """Register any plugins wanted to be in the PAU.
+        """
+
+    def unregister(pau):
+        """Unregister any plugins not wanted to be in the PAU.
+        """
+
+class IObjectConverter(Interface):
+    """Object converters are available as simple adapters, adapting
+       interfaces (not regular instances).
+
+    """
+
+    def fromStringDict(self, data_dict, context, form_fields=None):
+        """Convert values in `data_dict`.
+
+        Converts data in `data_dict` into real values based on
+        `context` and `form_fields`.
+
+        `data_dict` is a mapping (dict) from field names to values
+        represented as strings.
+
+        The fields (keys) to convert can be given in optional
+        `form_fields`. If given, form_fields should be an instance of
+        :class:`zope.formlib.form.Fields`. Suitable instances are for
+        example created by :class:`grok.AutoFields`.
+
+        If no `form_fields` are given, a default is computed from the
+        associated interface.
+
+        The `context` can be an existing object (implementing the
+        associated interface) or a factory name. If it is a string, we
+        try to create an object using
+        :func:`zope.component.createObject`.
+
+        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
+        <DATA_DICT>)`` where
+
+        ``<FIELD_ERRORS>``
+           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
+           error that happened when validating the input data in
+           `data_dict`
+
+        ``<INVARIANT_ERRORS>``
+           is a list of invariant errors concerning several fields
+
+        ``<DATA_DICT>``
+           is a dict with the values from input dict converted.
+
+        If errors happen, i.e. the error lists are not empty, always
+        an empty ``<DATA_DICT>`` is returned.
+
+        If ``<DATA_DICT>`` is non-empty, there were no errors.
+        """
+
+class IFieldConverter(Interface):
+    def request_data(name, value, schema_field, prefix='', mode='create'):
+        """Create a dict with key-value mapping as created by a request.
+
+        `name` and `value` are expected to be parsed from CSV or a
+        similar input and represent an attribute to be set to a
+        representation of value.
+
+        `mode` gives the mode of import.
+
+        :meth:`update_request_data` is then requested to turn this
+        name and value into vars as they would be sent by a regular
+        form submit. This means we do not create the real values to be
+        set but we only define the values that would be sent in a
+        browser request to request the creation of those values.
+
+        The returned dict should contain names and values of a faked
+        browser request for the given `schema_field`.
+
+        Field converters are normally registered as adapters to some
+        specific zope.schema field.
+        """
+
+class IObjectHistory(Interface):
+
+    messages = schema.List(
+        title = u'List of messages stored',
+        required = True,
+        )
+
+    def addMessage(message):
+        """Add a message.
+        """
+
+class IKofaWorkflowInfo(IWorkflowInfo):
+    """A :class:`hurry.workflow.workflow.WorkflowInfo` with additional
+       methods for convenience.
+    """
+    def getManualTransitions():
+        """Get allowed manual transitions.
+
+        Get a sorted list of tuples containing the `transition_id` and
+        `title` of each allowed transition.
+        """
+
+class ISiteLoggers(Interface):
+
+    loggers = Attribute("A list or generator of registered KofaLoggers")
+
+    def register(name, filename=None, site=None, **options):
+        """Register a logger `name` which logs to `filename`.
+
+        If `filename` is not given, logfile will be `name` with
+        ``.log`` as filename extension.
+        """
+
+    def unregister(name):
+        """Unregister a once registered logger.
+        """
+
+class ILogger(Interface):
+    """A logger cares for setup, update and restarting of a Python logger.
+    """
+
+    logger = Attribute("""A :class:`logging.Logger` instance""")
+
+
+    def __init__(name, filename=None, site=None, **options):
+        """Create a Kofa logger instance.
+        """
+
+    def setup():
+        """Create a Python :class:`logging.Logger` instance.
+
+        The created logger is based on the params given by constructor.
+        """
+
+    def update(**options):
+        """Update the logger.
+
+        Updates the logger respecting modified `options` and changed
+        paths.
+        """
+
+class ILoggerCollector(Interface):
+
+    def getLoggers(site):
+        """Return all loggers registered for `site`.
+        """
+
+    def registerLogger(site, logging_component):
+        """Register a logging component residing in `site`.
+        """
+
+    def unregisterLogger(site, logging_component):
+        """Unregister a logger.
+        """
+
+#
+# External File Storage and relatives
+#
+class IFileStoreNameChooser(INameChooser):
+    """See zope.container.interfaces.INameChooser for base methods.
+    """
+    def checkName(name, attr=None):
+        """Check whether an object name is valid.
+
+        Raises a user error if the name is not valid.
+        """
+
+    def chooseName(name, attr=None):
+        """Choose a unique valid file id for the object.
+
+        The given name may be taken into account when choosing the
+        name (file id).
+
+        chooseName is expected to always choose a valid file id (that
+        would pass the checkName test) and never raise an error.
+
+        If `attr` is not ``None`` it might been taken into account as
+        well when generating the file id. Usual behaviour is to
+        interpret `attr` as a hint for what type of file for a given
+        context should be stored if there are several types
+        possible. For instance for a certain student some file could
+        be the connected passport photograph or some certificate scan
+        or whatever. Each of them has to be stored in a different
+        location so setting `attr` to a sensible value should give
+        different file ids returned.
+        """
+
+class IExtFileStore(IFileRetrieval):
+    """A file storage that stores files in filesystem (not as blobs).
+    """
+    root = schema.TextLine(
+        title = u'Root path of file store.',
+        )
+
+    def getFile(file_id):
+        """Get raw file data stored under file with `file_id`.
+
+        Returns a file descriptor open for reading or ``None`` if the
+        file cannot be found.
+        """
+
+    def getFileByContext(context, attr=None):
+        """Get raw file data stored for the given context.
+
+        Returns a file descriptor open for reading or ``None`` if no
+        such file can be found.
+
+        Both, `context` and `attr` might be used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method.
+        """
+
+    def deleteFile(file_id):
+        """Delete file stored under `file_id`.
+
+        Remove file from filestore so, that it is not available
+        anymore on next call to getFile for the same file_id.
+
+        Should not complain if no such file exists.
+        """
+
+    def deleteFileByContext(context, attr=None):
+        """Delete file for given `context` and `attr`.
+
+        Both, `context` and `attr` might be used to find (`context`)
+        and feed (`attr`) an appropriate file name chooser.
+
+        This is a convenience method.
+        """
+
+    def createFile(filename, f):
+        """Create file given by f with filename `filename`
+
+        Returns a hurry.file.File-based object.
+        """
+
+class IFileStoreHandler(Interface):
+    """Filestore handlers handle specific files for file stores.
+
+    If a file to store/get provides a specific filename, a file store
+    looks up special handlers for that type of file.
+
+    """
+    def pathFromFileID(store, root, filename):
+        """Turn file id into path to store.
+
+        Returned path should be absolute.
+        """
+
+    def createFile(store, root, filename, file_id, file):
+        """Return some hurry.file based on `store` and `file_id`.
+
+        Some kind of callback method called by file stores to create
+        file objects from file_id.
+
+        Returns a tuple ``(raw_file, path, file_like_obj)`` where the
+        ``file_like_obj`` should be a HurryFile, a KofaImageFile or
+        similar. ``raw_file`` is the (maybe changed) input file and
+        ``path`` the relative internal path to store the file at.
+
+        Please make sure the ``raw_file`` is opened for reading and
+        the file descriptor set at position 0 when returned.
+
+        This method also gets the raw input file object that is about
+        to be stored and is expected to raise any exceptions if some
+        kind of validation or similar fails.
+        """
+
+class IPDF(Interface):
+    """A PDF representation of some context.
+    """
+
+    def __call__(view=None, note=None):
+        """Create a bytestream representing a PDF from context.
+
+        If `view` is passed in additional infos might be rendered into
+        the document.
+
+        `note` is optional HTML rendered at bottom of the created
+        PDF. Please consider the limited reportlab support for HTML,
+        but using font-tags and friends you certainly can get the
+        desired look.
+        """
+
+class IMailService(Interface):
+    """A mail service.
+    """
+
+    def __call__():
+        """Get the default mail delivery.
+        """
+
+
+class IDataCenterConfig(Interface):
+    path = Path(
+        title = u'Path',
+        description = u"Directory where the datacenter should store "
+                      u"files by default (adjustable in web UI).",
+        required = True,
+        )
+
+#
+# Asynchronous job handling and related
+#
+class IJobManager(IKofaObject):
+    """A manager for asynchronous running jobs (tasks).
+    """
+    def put(job, site=None):
+        """Put a job into task queue.
+
+        If no `site` is given, queue job in context of current local
+        site.
+
+        Returns a job_id to identify the put job. This job_id is
+        needed for further references to the job.
+        """
+
+    def jobs(site=None):
+        """Get an iterable of jobs stored.
+        """
+
+    def get(job_id, site=None):
+        """Get the job with id `job_id`.
+
+        For the `site` parameter see :meth:`put`.
+        """
+
+    def remove(job_id, site=None):
+        """Remove job with `job_id` from stored jobs.
+        """
+
+    def start_test_job(site=None):
+        """Start a test job.
+        """
+
+class IProgressable(Interface):
+    """A component that can indicate its progress status.
+    """
+    percent = schema.Float(
+        title = u'Percent of job done already.',
+        )
+
+class IJobContainer(IContainer):
+    """A job container contains IJob objects.
+    """
+
+class IExportJob(zc.async.interfaces.IJob):
+    def __init__(site, exporter_name):
+        pass
+
+    finished = schema.Bool(
+        title = u'`True` if the job finished.`',
+        default = False,
+        )
+
+    failed = schema.Bool(
+        title = u"`True` iff the job finished and didn't provide a file.",
+        default = None,
+        )
+
+class IExportJobContainer(IKofaObject):
+    """A component that contains (maybe virtually) export jobs.
+    """
+    def start_export_job(exporter_name, user_id, *args, **kwargs):
+        """Start asynchronous export job.
+
+        `exporter_name` is the name of an exporter utility to be used.
+
+        `user_id` is the ID of the user that triggers the export.
+
+        `args` positional arguments passed to the export job created.
+
+        `kwargs` keyword arguments passed to the export job.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started.
+        """
+
+    def get_running_export_jobs(user_id=None):
+        """Get export jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
+        that order. The ``<exporter_name>`` is the utility name of the
+        used exporter.
+
+        If `user_id` is ``None``, all running jobs are returned.
+        """
+
+    def get_export_jobs_status(user_id=None):
+        """Get running/completed export jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <exporter title>`` in that order, where ``<status
+        translated>`` and ``<exporter title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the exporter used.
+        """
+
+    def delete_export_entry(entry):
+        """Delete the export denoted by `entry`.
+
+        Removes `entry` from the local `running_exports` list and also
+        removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <exporter name>, <user id>)``
+        as created by :meth:`start_export_job` or returned by
+        :meth:`get_running_export_jobs`.
+        """
+
+    def entry_from_job_id(job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+
+class IExportContainerFinder(Interface):
+    """A finder for the central export container.
+    """
+    def __call__():
+        """Return the currently used global or site-wide IExportContainer.
+        """
+
+class IFilteredQuery(IKofaObject):
+    """A query for objects.
+    """
+
+    defaults = schema.Dict(
+        title = u'Default Parameters',
+        required = True,
+        )
+
+    def __init__(**parameters):
+        """Instantiate a filtered query by passing in parameters.
+        """
+
+    def query():
+        """Get an iterable of objects denoted by the set parameters.
+
+        The search should be applied to objects inside current
+        site. It's the caller's duty to set the correct site before.
+
+        Result can be any iterable like a catalog result set, a list,
+        or similar.
+        """
+
+class IFilteredCatalogQuery(IFilteredQuery):
+    """A catalog-based query for objects.
+    """
+
+    cat_name = schema.TextLine(
+        title = u'Registered name of the catalog to search.',
+        required = True,
+        )
+
+    def query_catalog(catalog):
+        """Query catalog with the parameters passed to constructor.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/language.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/language.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/language.py	(revision 16045)
@@ -0,0 +1,44 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Handle language as requested by users.
+"""
+import grokcore.component
+from zope.component import getUtility
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.session.interfaces import ISession
+from zope.i18n.interfaces import IUserPreferredLanguages
+from waeup.kofa.configuration import ConfigurationContainer
+from waeup.kofa.interfaces import IKofaUtils
+
+class KofaLanguage(grokcore.component.Adapter):
+    """Set preferred languages"""
+    grokcore.component.context(IBrowserRequest)
+    grokcore.component.implements(IUserPreferredLanguages)
+
+    def getPreferredLanguages(self):
+        # This an adapter for the request, so self.context is the request.
+        request = self.context
+
+        # Extract the preferred language from a cookie:
+        lang = request.cookies.get('kofa.language', None)
+        if lang is None:
+            lang = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            request.response.setCookie('kofa.language', lang, path='/')
+
+        # According to IUserPreferredLanguages, we must return a list.
+        return [lang]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales.zcml	(revision 16045)
@@ -0,0 +1,7 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+
+  <include package="zope.i18n" file="meta.zcml" />
+  <i18n:registerTranslations directory="locales" />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,7088 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2012-10-15 13:21+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: de-DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: German\n"
+"X-Poedit-Country: GERMANY\n"
+"X-Poedit-SourceCharset: utf-8\n"
+"X-Poedit-Basepath: \\\\Salsa\\kofa\\trunk\\src\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr "Zugangscodeerzeugung abgebrochen."
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr "Reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr "Reimport abgebrochen."
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr "Keine Datei ausgewählt, Aktion abgebrochen."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr "Diese Serie existiert bereits: ${a}"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr "Erfolgreich reimportiert: ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr "Suche und verwalte Zugangscodes"
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr "Suchen"
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr "Zugangscode deaktivieren"
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr "Zugangscode aktivieren"
+
+#: waeup/kofa/accesscodes/browser.py:157
+#, fuzzy
+msgid "Cancel Search"
+msgstr "Abbruch"
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr "deaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr "${a} deaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr "${a}: Deaktivierung nicht möglich."
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr "reaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr "${a} (re-)aktiviert."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr "${a}: Reaktivieren nich möglich."
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr "Zugangscodeserie hinzufügen"
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr "Zugangscode suchen"
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr "Zugangscodes"
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr "Archivieren"
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr "Archivieren und löschen"
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr "Zugangscodeserien"
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr "Keine Serie ausgewählt."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr "${a} (${b}) archiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr "Serie ${a} gelöscht"
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr "Zugangscodeserie erzeugen"
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr "Serie erzeugen"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr "Serie ${a} mit Einträgen erzeugt."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr "Daten in ${a} gespeichert."
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr "Einträge"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr "Verwendet"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+#, fuzzy
+msgid "Disabled"
+msgstr "deaktiviert"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr "Preis"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr "Erzeugungsdatum"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr "Die folgenden Serien sind vorhanden:"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr "Keine Serie vorhanden"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr "Prefix"
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr "Keine Importserien verfügbar"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr "mit PIN"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr "mit Seriennummer"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr "mit Mitteilung"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr "Seriennummer"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr "Zugangscode"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr "nach Zugangscodes"
+
+#: waeup/kofa/accesscodes/export.py:38
+#, fuzzy
+msgid "AccessCodeBatches"
+msgstr "Zugangscodeserien"
+
+#: waeup/kofa/accesscodes/export.py:69
+#, fuzzy
+msgid "AccessCodes"
+msgstr "Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr "Seriennummer innerhalb einer Serie"
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr "Prefix innerhalb einer Serie"
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr "Batch-Nummer"
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr "Zufallsteil des Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr "Preis des Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr "Workflow-Status"
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr "Vollständiger Titel des Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr "Käufer"
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr "Die Historie eines Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr "Erzeuger"
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr "Serien-Prefix"
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr "Seriennummer (1-3 Zeichen)"
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr "Zugangscode-Nummer"
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr "Anzahl der deaktivierten Zugangscodes"
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr "Anzahl der verbrauchten Zugangscodes"
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr "Benutzte PIN deaktivieren"
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr "Deaktivierte PIN reaktivieren"
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr "initialisiert"
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr "verwendet"
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr "PIN initialisieren"
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr "PIN verwenden"
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr "Unbenutzte PIN deaktivieren"
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "Applicant has not yet been admitted."
+msgstr "Antrafsteller wurde noch nicht zugelassen."
+
+#: waeup/kofa/applicants/applicant.py:134
+msgid "Registration Number exists."
+msgstr "Regsistrierungsnummer existiert."
+
+#: waeup/kofa/applicants/applicant.py:137
+msgid "No course admitted provided."
+msgstr "Es wurde noch kein Studiengang zugewiesen."
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+msgid "Student ${a} created"
+msgstr "Student ${a}  erzeugt"
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr "Antragsteller-Datensatz importiert"
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr "Zustand '${a}' gesetzt"
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr "Dieses Formular ist gesperrt."
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+#, fuzzy
+msgid "Find applicants"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr "Es wurde noch kein Passbild hochgeladen."
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr "Bitte bestätigen Sie das Passbild."
+
+#: waeup/kofa/applicants/browser.py:103
+#, fuzzy
+msgid "Find applicant"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+"Dieses Formular kann nicht abgesendet werden, da Sie den falschen "
+"Antragsstatus haben."
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr "Das Formular wurde eingereicht."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr "Registrierung für ${a}"
+
+#: waeup/kofa/applicants/browser.py:1120
+msgid "Outside application period."
+msgstr "Außerhalb des Antragszeitraums."
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+msgid "Send login credentials to email address"
+msgstr "Zugangsdaten an Email-Adresse senden"
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+msgid "An error occurred."
+msgstr "Es ein Fehler auf."
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+msgid "No application record found."
+msgstr "Es wurde kein passender Datensatz gefunden."
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+"Das Passwort wurde bereits gesetzt und benutzt. Bitte gehen Sie zu Login-"
+"Seite."
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr "Leerer Suchtext"
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr "Sie haben sich erfolgreich registriert für"
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr "Ihre Registrierung war erfolgreich."
+
+#: waeup/kofa/applicants/browser.py:122
+msgid "No applicant found."
+msgstr "Es wurde kein Antragsteller gefunden."
+
+#: waeup/kofa/applicants/browser.py:1222
+#, fuzzy
+msgid "Data Exports"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+#, fuzzy
+msgid "Discarded export"
+msgstr "Verworfene Export-Ergebnisse"
+
+#: waeup/kofa/applicants/browser.py:1258
+#, fuzzy
+msgid "Export started."
+msgstr "Import abgebrochen."
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr "Antragstellungsverfahren verwalten"
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr "Speichern"
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr "Ausgewählte Objekte löschen"
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr "Ordner für Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr "Ausgewählte lokale Rollen löschen"
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr "Lokale Rolle hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr "Ordner für Antragsteller"
+
+#: waeup/kofa/applicants/browser.py:159
+msgid "No container selected!"
+msgstr "Kein Container ausgewählt."
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr "Konnte nicht gelöscht werden:"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr "Erfolgreich entfernt: ${a}"
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr "Das Formular wurde gespeichert."
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+"Ein Ordner für diesen Antragstypen mit gleichem Eingangsjahr existiert "
+"bereits."
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr "Hinzugefügt:"
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr "Antragsteller"
+
+#: waeup/kofa/applicants/browser.py:258
+#, fuzzy
+msgid "Applicant Data Exports"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+msgid "Create students from selected"
+msgstr "Wandel ausgewählte Antragsteller in Studenten um"
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr "Nichts ausgewählt!"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+msgid "${a} students successfully created."
+msgstr "Student ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+msgid "No student could be created."
+msgstr "Es konnte kein Student erzeugt werden."
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr "Antrag hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr "Antrag wurde erzeugt."
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr "Antragsteller"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr "gesetzt"
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr "ungesetzt"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+msgid "${a} <br /> Application Record ${b}"
+msgstr "${a} <br /> Antrag ${b}"
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#: waeup/kofa/applicants/browser.py:66
+#, fuzzy
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+"'Nach dem Absenden können Sie dieses Formular nicht mehr bearbeiten. Wollen "
+"Sie wirklich absenden?'"
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr "Bezahl-Ticket wurde erzeugt."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr "${a}: Online-Bezahl-Ticket ${b}"
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr "Zahlung"
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+msgid "Online Payment Slip"
+msgstr "Quittung"
+
+#: waeup/kofa/applicants/browser.py:742
+#, fuzzy
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr "Sie müssen die Gebühr bezahlen, um den Antrag herunterladen zu können."
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr "Antragstellung"
+
+#: waeup/kofa/applicants/browser.py:763
+#, fuzzy
+msgid "Please pay and submit before trying to download the application slip."
+msgstr "Sie müssen die Gebühr bezahlen, um den Antrag herunterladen zu können."
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr "jpg-Dateiendung erwartet."
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr "Endgültig einreichen"
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr "Ausgewählte Tickets löschen"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+msgid "${a} <br /> Application Form ${b}"
+msgstr "${a} <br /> Antragsformular ${b}"
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr "Keine Änderung"
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr "Nichts ausgewählt!"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr "Containerstatistik"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr "Anzahl der eingereichten Anträge"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr "Status der Antragstellung"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr "Anzahl der Antragsteller"
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr "Zugelassen für Studienfach:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr "Passbild:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr "Max. Dateigröße:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr "Zustandsänderung der Antragstellung:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+"Ich bestätige, dass die hochgeladene Fotografie ein naturgetreues Portrait "
+"von mir ist."
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr "(Konto gesperrt)"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+#, fuzzy
+msgid "Payment Tickets"
+msgstr "Zahlungen"
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+"Ein Benutzerkonto wird für Sie eingerichtet. Anschließend werden die die "
+"Anmeldedaten an die von Ihnen zur Verfügung gestellte Emailadresse versendet."
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+"Wenn Ihr Datensatz gefunden wurde, werden die Zugangsdaten an die zur "
+"Verfügung gestellte Emailadresse versendet."
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr "Antragsnummer"
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr "Erste Wahl"
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+"Es sind mehr als 5000 Datensätze in diesem Container. Um lange Wartezeiten "
+"beim Herunterladen zu vermeiden, wurde die Auflistung der Datensätze "
+"deaktiviert. Bitte nutzen Sie das ${search_link}, um Antragsdatensätze zu "
+"finden."
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr "Suchformular"
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+"Bitte beachten Sie, dass die Container hier im manage-Modus geöffnet werde. "
+"Damit werden die Links zu allen Antragstellern in den Browser geladen. Das "
+"kann je nach Internetverbindung recht lange dauern und erfordert "
+"möglicherweise etwas Geduld."
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr "Jahr"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr "Anzahl"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr "Einleitung"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr "Beantragungsszeitraum"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr "mit Emailadresse"
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr "Basisordner für Antragsteller"
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Create Application Records"
+msgstr "Anträge hinzufügen"
+
+#: waeup/kofa/applicants/interfaces.py:145
+msgid "Update Application Records"
+msgstr "Anträge aktualisieren"
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+msgid "Human readable description in HTML format"
+msgstr "Lesbare Beschreibung im HTML-Format"
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr "Kürzel"
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr "Titel"
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr "Antragsobjekt (Programm)"
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr "Eingangsjahr"
+
+#: waeup/kofa/applicants/interfaces.py:219
+msgid "Application Mode"
+msgstr "Modus der Antragstellung"
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr "Kategorie für die Gruppierung von Zertifikaten"
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr "Beginn der Antragsperiode"
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr "Beispiel:"
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr "Ende der Antragsperiode"
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr "Antragstellung nach Ablauf der Antragsfrist verbieten"
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+msgid "Application Fee"
+msgstr "Antragsgebühr"
+
+#: waeup/kofa/applicants/interfaces.py:276
+#, fuzzy
+msgid "Hide container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr "Konto gesperrt"
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr "Antragsteller Id"
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr "Vorname"
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr "Zweiter Vorname"
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr "Nachname"
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr "Geburtsdatum"
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr "Geschlecht"
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr "Emailadresse"
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr "Telefon"
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr "Studienfach (1. Wahl)"
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr "Studienfach (2. Wahl)"
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr "Bemerkung"
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr "Studenten Id"
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr "Zugelassenes Studienfach"
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr "Formular gesperrt"
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+#, fuzzy
+msgid "Special Application"
+msgstr "Antragstellung"
+
+#: waeup/kofa/applicants/interfaces.py:520
+#, fuzzy
+msgid "Identification Number"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+msgid "Payment approved"
+msgstr "Zahlung bestätigt"
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr "Erfolgreiche Zahlung"
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr "Antrag"
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+#, fuzzy
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+"Gehen Sie zur Login-Seite des Portals und geben Sie die neuen Zugansdaten "
+"ein: Benutzername= ${a}, Passwort = ${b}. Ändern Sie Ihr Passwort, wenn Sie "
+"sich eingeloggt haben."
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Desired Study Courses"
+msgstr "Gewünschter Studiengang"
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr "Prozessdaten"
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr "Session-Konfiguration ist nicht verfügbar."
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+msgid "Amount could not be determined."
+msgstr "Betrag konnte nicht ermittelt werden."
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr "Für Antragstellung registrieren"
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr "Containerstatistic"
+
+#: waeup/kofa/applicants/viewlets.py:137
+msgid "Manage container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/kofa/applicants/viewlets.py:147
+#, fuzzy
+msgid "Export applicants"
+msgstr "nach Antragsteller"
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+#, fuzzy
+msgid "Create students"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr "Antrag anschauen"
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr "Antrag verwalten"
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr "Antrag bearbeiten"
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr "Antrag herunterladen"
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr "Quittung herunterladen"
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+msgid "Approve payment"
+msgstr "Zahlung bestätigen"
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr "Antragstellung"
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr "Meine Daten"
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr "Student erzeugt"
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr "Antrag zurücksetzen"
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr "Antrag zurückgesetzt"
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr "Antrag zurück auf 'Gebühren bezahlt' zurücksetzen"
+
+#: waeup/kofa/applicants/workflow.py:144
+msgid "Application reset to paid"
+msgstr "Antrag zurückgesetzt auf 'bezahlt'"
+
+#: waeup/kofa/applicants/workflow.py:157
+#, fuzzy
+msgid "Reset application to admitted"
+msgstr "Antrag zurück auf 'Gebühren bezahlt' zurücksetzen"
+
+#: waeup/kofa/applicants/workflow.py:158
+#, fuzzy
+msgid "Application reset to admitted"
+msgstr "Antrag zurückgesetzt auf 'bezahlt'"
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr "begonnen"
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr "bezahlt"
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr "eingereicht"
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr "zugelassen"
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr "nicht zugelassen"
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr "neu"
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr "Antrag initialisieren"
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr "Antrag initialisiert"
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr "Antragsvorgang starten"
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr "Antragsvorgang gestartet"
+
+#: waeup/kofa/applicants/workflow.py:66
+msgid "Pay application fee"
+msgstr "Antragsgebühr bezahlen"
+
+#: waeup/kofa/applicants/workflow.py:67
+msgid "Payment made"
+msgstr "Zahlung durchgeführt"
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr "Antrag einreichen"
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr "Antrag eingereicht"
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr "Antragsteller zulassen"
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr "Antragsteller zugelassen"
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr "Antrag ablehnen"
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr "Antrag abgelehnt"
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr "Bitte warten..."
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr "Portaleinstellungen"
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr "Akademie"
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr "Verwaltung"
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr "Portalkonfiguration"
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr "Benutzer"
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr "Datenzentrum"
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+#, fuzzy
+msgid "Reports"
+msgstr "Reimportieren"
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+#, fuzzy
+msgid "Student Data Exports"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr "Startseite"
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr "Kennung"
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+"Benutzername, Studenten Id, Antragsteller Id, Matrikelnummer oder "
+"Registrierungsnummer"
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+#, fuzzy
+msgid "Are you sure?"
+msgstr "'Sind Sie sicher?'"
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr "Keine lokalen Rollen ausgewählt."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr "Alle Import-Manager wurden per Email benachrichtigt."
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/kofa/browser/pages.py:1030
+#, fuzzy
+msgid "Maximum number of files in the data center exceeded."
+msgstr "Maximale Anzahl der Credit-Punkte von ${a} überschritten."
+
+#: waeup/kofa/browser/pages.py:1045
+msgid "Only csv files are allowed."
+msgstr "Es sind nur csv-Dateien erlaubt."
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr "Eine Datei gleichen Namens wurde bereits hochgeladen."
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr "Zurück zu Schritt 1"
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr "Gehe zu Schritt 3"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr "Lokale Rolle erfolgreich entfernt: ${a}"
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr "Doppelte Spaltenüberschrift"
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr "Tauschen Sie die Importdatei aus!"
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr "Import abgebrochen."
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr "Nur Aktualisierungsmodus!"
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr "Spaltenüberschrift setzen"
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr "Zurück zu Schritt 2"
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr "Import durchführen"
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr "Nichts ausgewählt."
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr ""
+"Bearbeiten Sie die Spaltenüberschrift oder tauschen Sie die Importdatei aus."
+
+#: waeup/kofa/browser/pages.py:1422
+#, fuzzy
+msgid "Process next"
+msgstr "Prozessorname"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr "Bearbeitung von ${a} Zeilen fehlgeschlagen."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr "${a} Zeilen erfolgreich verarbeitet."
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr "Logdateien anzeigen"
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr "Zeigen"
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr "Geben Sie einen regulären Ausdruck ein ..."
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+"Logdateien können nur auf Unix-basierten Betriebsystem durchsucht werden."
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr "Ungültiger Suchausdruck"
+
+#: waeup/kofa/browser/pages.py:1528
+msgid "No search results found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr "Einstellungen des Datenzentrums bearbeiten"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr "Datei existiert (nicht kopiert): ${a}"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+#, fuzzy
+msgid "Given storage path cannot be used. ${a}"
+msgstr "Der Datenpfad kann nicht verwendet werden."
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr "Neuen Datenpfad erfolgreich gesetzt."
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr "Portaldaten als CSV-Datei herunterladen"
+
+#: waeup/kofa/browser/pages.py:1577
+#, fuzzy
+msgid "Create CSV file"
+msgstr "CSV-Datei exportieren"
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr "Akademie"
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr "Fakultät hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr "Fakultäten"
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr "Akademie verwalten"
+
+#: waeup/kofa/browser/pages.py:1721
+#, fuzzy
+msgid "You are not allowed to remove entire faculties."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr "Das Fakultätskürzel existiert bereits."
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr "Departments"
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr "Department hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr "Fakultät verwalten"
+
+#: waeup/kofa/browser/pages.py:1807
+#, fuzzy
+msgid "You are not allowed to remove entire departments."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+#, fuzzy
+msgid "Find student(s)"
+msgstr "Student hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1847
+#, fuzzy
+msgid "Find students in "
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr "Das Kürzel existiert bereits in dieser Fakultät."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr "Department ${a} hinzugefügt."
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr "Kurse und Zertifikate"
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr "Ausgewählte Kurse löschen"
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr "Kurs hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr "Ausgewählte Zertifikate löschen"
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr "Zertifikat hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr "Department verwalten"
+
+#: waeup/kofa/browser/pages.py:1996
+#, fuzzy
+msgid "You are not allowed to remove certificates."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr "Ein Kurs mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr "Kurs ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr "Ein Zertifikat mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr "Zertifikat ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr "Kurs bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr "Zertifikat bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+msgid "Add certificate course"
+msgstr "Zertifikat-Kurs hinzufügen"
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+msgid "Remove selected certificate courses"
+msgstr "Ausgewählte Zertifikat-Kurse löschen"
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr "Zertifikat verwalten"
+
+#: waeup/kofa/browser/pages.py:2244
+msgid "The chosen certificate course is already part of this certificate."
+msgstr "Der ausgewählte Zertifikat-Kurs ist bereits Teil des Zertifikates."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+msgid "certificate course ${a}_${b} added."
+msgstr "Zertifikat-Kurs ${a}_${b} hinzugefügt."
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+msgid "Edit certificate course"
+msgstr "Zertifikat-Kurs bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr "Speichen und zurück"
+
+#: waeup/kofa/browser/pages.py:2325
+msgid "Send me a new password"
+msgstr "Neues Passwort zusenden"
+
+#: waeup/kofa/browser/pages.py:2373
+msgid "No record found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+msgid "You have successfully requested a password for the"
+msgstr "Sie haben erfolgreich ein Passwort angefordert für das"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+"Eine Email mit Ihren Benutzernamen und Ihrem Passwort wurde versendet an "
+"${a}."
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr "Einloggen"
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr "Sie sind jetzt eingeloggt."
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+#, fuzzy
+msgid "Your account has been temporarily deactivated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/browser/pages.py:324
+#, fuzzy
+msgid "Your account has been deactivated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/browser/pages.py:327
+#, fuzzy
+msgid "You entered invalid credentials."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/browser/pages.py:342
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr "Sie wurden ausgeloggt. Danke dass Sie WAeUP Kofa benutzt haben."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr "Jetzt senden"
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr "Ihre Nachricht wurde verschickt."
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr "Sende jetzt"
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/kofa/browser/pages.py:469
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr "<h1>Willkommen bei WAeUP.Kofa</h1>"
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr "Verwalten"
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr "Entfernen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr "Benutzerkonto ${a} erfolgreich gelöscht."
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr "Benutzer hinzufügen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr "Benutzerkonto ${a} erfolgreich hinzugefügt."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr "Benutzer bearbeiten"
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr "Benutzereinstellungen wurden gespeichert."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr "Sende Nachricht an ${a}"
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr "Meine Präferenzen"
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr "Meine Rollen"
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr "Portal-Konfiguration betrachten"
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr "Portalkonfiguration bearbeiten"
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr "Plugins aktualisieren"
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr "Session ${a}-Konfiguration"
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr "Plugins wurden aktualisiert, siehe Logdatei."
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr "Die Session existiert bereits."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr "Session ${a}-Konfiguration bearbeiten"
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr "OSError: Die Datei konnte nicht gelöscht werden."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+msgid "Successfully deleted: ${a}"
+msgstr "Successfully deleted: ${a}"
+
+#: waeup/kofa/browser/pages.py:932
+#, fuzzy
+msgid "Processed Files"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr "Zurück zum Datenzentrum"
+
+#: waeup/kofa/browser/pages.py:946
+#, fuzzy
+msgid "Upload portal data as CSV file"
+msgstr "Portaldaten als CSV-Datei herunterladen"
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr "Hochladen"
+
+#: waeup/kofa/browser/pages.py:995
+#, fuzzy
+msgid "Import Manager"
+msgstr "Import abgebrochen."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+#, fuzzy
+msgid "${a}: ${b} uploaded"
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/kofa/browser/pdf.py:156
+#, fuzzy
+msgid "Date:"
+msgstr "Datum"
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr "Datum"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr "${footer_text} Seite ${num1} of ${num2}"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+msgid "Date: ${a}"
+msgstr "Datum: ${a}"
+
+#: waeup/kofa/browser/reports.py:112
+#, fuzzy
+msgid "Create report"
+msgstr "Erzeuger"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr "Kurskürzel:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr "Kursetitel:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr "Angeboten von:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr "Level:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+#, fuzzy
+msgid "Required course:"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr "Kürzel:"
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr "Zertifikat-Kurs"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr "Semester"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr "Cert.-Kurs"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr "Dep.-Kurs"
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+"Geben Sie Ihre im System gespeicherte Emailadresse an. Sie können die "
+"Adresse auf diesem Wege nicht ändern. Wenn Sie die Adresse vergessen haben "
+"oder die Adresse nicht mehr gültig sein sollte, nutzen Sie das "
+"Kontaktformular und beantragen Sie einen Adresswechsel."
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen:"
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr "Einstellungen"
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr "Session-Konfiguration"
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen."
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr "Hochladedatum"
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr "Schritt 1"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Dateiname"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr "Datensätze"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr "Dateigröße"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+"Im Datenzentrum können Sie durch den Import von CSV-Dateien Daten des "
+"Portals massenhaft erzeugen aktualisieren oder entfernen."
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr "Auswählen"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr "Wählen Sie eine Datei aus der Liste unten zum Import aus."
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr "Schritt 2"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr "Überschrift"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr "Beispielsdatensatz 1"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr "Beispielsdatensatz 2"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr "Beispielsdatensatz 3"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr "Modus:"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+"Wählen Sie einen Datei-Prozessor und einen Verarbeitungsmodus aus der Liste "
+"unten aus."
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr "Spaltenüberschriften in Ordnung"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr "ändern zu:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr "Beispielsdatensatz"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr "Schritt 3"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr "Ändern Sie die Spaltenüberschriften der Importdateien."
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr "Schritt 4"
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr "Verarbeitungsmodus:"
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr "Stapelverarbeitung beendet."
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr "Zur Zeit sind keine Logdateien vorhanden."
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+#, fuzzy
+msgid "Storage path"
+msgstr "Pfad des Speicherorts:"
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr "Das Datenzentrum dient dem Hochladen und Importieren von CSV-Daten."
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Pfad des Speicherorts:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Datei:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr "Prozessor:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+#, fuzzy
+msgid "Import Mode:"
+msgstr "Import abgebrochen."
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr "Zur Verfügung Prozessoren"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+#, fuzzy
+msgid "Processor"
+msgstr "Prozessor:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr "Erforderliche Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr "Optionale Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr "Nicht-Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+#, fuzzy
+msgid "Download CSV Skeleton File"
+msgstr "CSV-Gerüst"
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr "Hochgeladene Dateien"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr "Lokale Rolle"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr "Lokale Rollen"
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr "Es existieren bisher keine Unterobjekte."
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+#, fuzzy
+msgid "Password"
+msgstr "Passwort:"
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr "Groß- / Kleinschreibung (beachten)! "
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr ""
+"Vergessen Sie nicht, sich vor dem Verlassen des Portals auszuloggen. Wenn "
+"Sie Probleme beim Einloggen haben, stellen Sie zunächst sicher, dass Cookies "
+"im Browser aktiviert sind."
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr ""
+"Wenn Sie ein neuer Student sind und noch kein Benutzerkonto haben, oder Ihr "
+"Datensatz gerade frisch  erzeugt wurde, erwerben Sie bitte einen PWD-"
+"Zugangscode und initialisieren Sie Ihr Benutzerkonto <strong><a href="
+"\"setpassword\"> hier</a></strong>."
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+msgid "login_trouble4"
+msgstr ""
+"Wenn Sie ein neuer Student sind und noch kein Benutzerkonto haben, oder Ihr "
+"Datensatz gerade frisch erzeugt wurde, initialisieren Sie Ihr Benutzerkonto "
+"<strong><a href=\"requestpw\"> hier</a></strong>."
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr ""
+"Oder haben Sie Ihre Sudenten Id und/oder Ihr Passwort vergessen? Dann "
+"beantragen Sie <strong><a href=\"changepw\">hier</a></strong> ein neues "
+"Passwort."
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+#, fuzzy
+msgid "User Name or Id"
+msgstr "Benutzername oder Id:"
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr "Meine portalweiten Rollen:"
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr "Meine lokalen Rollen:"
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr "Diese Seite steht nicht zur Verfügung."
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr "Sie versuchen eine nicht existierende Seite aufzurufen."
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr "Bitte beachten Sie:"
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr "Sie haben möglicherweise die URL falsch eingetragen."
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+#, fuzzy
+msgid "Report Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+#, fuzzy
+msgid "Description"
+msgstr "Beschreibung/Nachricht"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+#, fuzzy
+msgid "Create new report"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr "Kontakt"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr "Ausloggen"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr "WAeUP.Kofa - Student Management System"
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr "Any link text"
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr "Email versenden"
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr "Einstellungen bearbeiten"
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+#, fuzzy
+msgid "Export student data"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+#, fuzzy
+msgid "Find students"
+msgstr "Student hinzufügen"
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr "Studenten zeigen"
+
+#: waeup/kofa/browser/viewlets.py:518
+#, fuzzy
+msgid "Update scores"
+msgstr "Kurse validieren"
+
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Show logs"
+msgstr "Logdateien anzeigen"
+
+#: waeup/kofa/browser/viewlets.py:583
+#, fuzzy
+msgid "Upload data"
+msgstr "Hochladedatum"
+
+#: waeup/kofa/browser/viewlets.py:592
+#, fuzzy
+msgid "Process data"
+msgstr "Prozessdaten"
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/browser/viewlets.py:610
+#, fuzzy
+msgid "View processed files"
+msgstr "Logdatei betrachten"
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr "Anfragen"
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr "Wohnheime"
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr "Wohnheime verwalten"
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr "Wohnheim hinzufügen"
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+#, fuzzy
+msgid "Clear all hostels"
+msgstr "Wohnheim erzeugen"
+
+#: waeup/kofa/hostels/browser.py:160
+#, fuzzy
+msgid "All hostels cleared."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr "Einstellungen wurden gespeichert."
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr "Wohnheim erzeugen"
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr "Das Wohnheim existiert bereits."
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr "Wohnheim verwalten"
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr "Alle Betten aktualisieren"
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr "Reservierung der ausgewählten Betten ändern"
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr "Ausgewählte Betten freigeben"
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+#, fuzzy
+msgid "Clear hostel"
+msgstr "Wohnheim erzeugen"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+"${a} leere Betten entfernt, ${b} Betten hinzugefügt, ${c} belegte Betten "
+"geändert (${d})"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr "Erfolgreich geänderte Betten: ${a}"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr "Erfolgreich freigegebene Betten: ${a}"
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr "Keine Betten ausgewählt."
+
+#: waeup/kofa/hostels/browser.py:320
+#, fuzzy
+msgid "Hostel cleared."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr "Student zuweisen"
+
+#: waeup/kofa/hostels/browser.py:339
+#, fuzzy
+msgid "No valid student id."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr "Wohnheime"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr "Block ${a}, Raum ${b}, Bett ${c}"
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr "Id"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr "Name"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+#, fuzzy
+msgid "Booked Beds"
+msgstr "Bett B"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr "Typ"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr "Nummer"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr "Eigentümer"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr "Wohnheimdaten"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr "[Student zuweisen]"
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+#, fuzzy
+msgid "Reserved Beds:"
+msgstr "Reservierte Betten"
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr "Betten"
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr "nicht reserviert"
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr "reserviert"
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr "Etagen pro Block"
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr "Räume pro Flur"
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr "Reservierte Betten"
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr "Blöcke für Frauen"
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr "Blöcke für Männer"
+
+#: waeup/kofa/hostels/interfaces.py:141
+#, fuzzy
+msgid "Beds for Pre-Study Students"
+msgstr "Betten für Erstsemester"
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr "Betten für Erstsemester"
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr "Betten für rückgemeldete Studenten"
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr "Betten für Studenten im letzten Studienjahr"
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr "Unkategorisierte Betten"
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr "Spezielle Handhabung"
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr "Blöcke für Männer und Frauen überlappen."
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr "Bettkategorien überlappen."
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr "Betten Id"
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr "Betttyp"
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr "Bettnummer"
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr "Eigentümer (Student)"
+
+#: waeup/kofa/hostels/interfaces.py:243
+#, fuzzy
+msgid "Enter valid student id."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/hostels/interfaces.py:258
+#, fuzzy
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+"Sie sind noch nicht in der Session angekommen, für die aktuell die "
+"Bettenbuchung aktiviert ist."
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+msgid "Hostel Allocation Start Date"
+msgstr "Beginn der Bettenbuchung"
+
+#: waeup/kofa/hostels/interfaces.py:43
+msgid "Hostel Allocation Closing Date"
+msgstr "Ende der Bettenbuchung"
+
+#: waeup/kofa/hostels/interfaces.py:49
+msgid "Booking Session"
+msgstr "Bettenbuchung-Session"
+
+#: waeup/kofa/hostels/interfaces.py:57
+msgid "Allowed States"
+msgstr "Erlaubte Zustände"
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr "Wohnheim Id"
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr "Sortierungsnummer"
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr "Wohnheimname"
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr "Bett A"
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr "Bett B"
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr "Bett C"
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr "Bett D"
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr "Bett E"
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr "Bett F"
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr "Bett G"
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr "Bett H"
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr "Bett I"
+
+#: waeup/kofa/hostels/vocabularies.py:47
+#, fuzzy
+msgid "Bed J"
+msgstr "Bett A"
+
+#: waeup/kofa/hostels/vocabularies.py:48
+#, fuzzy
+msgid "Bed K"
+msgstr "Bett A"
+
+#: waeup/kofa/hostels/vocabularies.py:49
+#, fuzzy
+msgid "Bed L"
+msgstr "Bett A"
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr "Block B"
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr "Block C"
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr "Block D"
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr "Block E"
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr "Block F"
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr "Block G"
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr "Block H"
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr "Block I"
+
+#: waeup/kofa/hostels/vocabularies.py:62
+#, fuzzy
+msgid "Block J"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr "Block K"
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr "Block L"
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr "Block M"
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr "Block N"
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr "Block O"
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr "Block P"
+
+#: waeup/kofa/hostels/vocabularies.py:69
+#, fuzzy
+msgid "Block Q"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:70
+#, fuzzy
+msgid "Block R"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:71
+#, fuzzy
+msgid "Block S"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:72
+#, fuzzy
+msgid "Block T"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:73
+#, fuzzy
+msgid "Block U"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:74
+#, fuzzy
+msgid "Block V"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:75
+#, fuzzy
+msgid "Block W"
+msgstr "Block A"
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr "Kurse registriert"
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr "Kurse validiert"
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr "rückgemeldet"
+
+#: waeup/kofa/interfaces.py:103
+#, fuzzy
+msgid "graduated"
+msgstr "neu"
+
+#: waeup/kofa/interfaces.py:104
+#, fuzzy
+msgid "transcript requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr "Fach"
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr "Note"
+
+#: waeup/kofa/interfaces.py:399
+msgid "Processor name"
+msgstr "Prozessorname"
+
+#: waeup/kofa/interfaces.py:425
+msgid "Email Address:"
+msgstr "Emailadresse:"
+
+#: waeup/kofa/interfaces.py:432
+msgid "Email to:"
+msgstr "Email an:"
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr "Betreff:"
+
+#: waeup/kofa/interfaces.py:443
+msgid "Full Name:"
+msgstr "Vollständiger Name:"
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr "Text:"
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+msgid "Public Name"
+msgstr "Öffentlicher Name"
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+#, fuzzy
+msgid "Number of failed logins"
+msgstr "Zugangscode-Nummer"
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr "Benutzer Id"
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr "Vollständiger Name"
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr "Beschreibung/Nachricht"
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Portalweite Rollen:"
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of University"
+msgstr "Name der Universität"
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr "Deomo-Universität"
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr "Abkürzung"
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr "Inhalt im HTML-Format"
+
+#: waeup/kofa/interfaces.py:626
+msgid "Name of Administrator"
+msgstr "Name des Administrators"
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr "neu"
+
+#: waeup/kofa/interfaces.py:632
+msgid "Email Address of Administrator"
+msgstr "Emailadresse des Administrators"
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr "Betreff der Email an Administrator"
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr "abgeschlossen"
+
+#: waeup/kofa/interfaces.py:640
+msgid "Kofa Contact"
+msgstr "Kofa-Kontakt"
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr "SMTP Dienst"
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr "unerledigt"
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr "Captcha für öffentliche Registrierung"
+
+#: waeup/kofa/interfaces.py:659
+msgid "Carry-over Course Registration"
+msgstr "Carry-Over-Kursregistrierung"
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr "aktiv"
+
+#: waeup/kofa/interfaces.py:664
+#, fuzzy
+msgid "Current Academic Session"
+msgstr "Akademische Session"
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr "zugewiesen"
+
+#: waeup/kofa/interfaces.py:673
+#, fuzzy
+msgid "Next Matriculation Number Integer"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+#, fuzzy
+msgid "callbacks"
+msgstr "Bitte um Rückruf"
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+msgid "Academic Session"
+msgstr "Akademische Session"
+
+#: waeup/kofa/interfaces.py:700
+#, fuzzy
+msgid "Acceptance Fee"
+msgstr "Acceptance Letter"
+
+#: waeup/kofa/interfaces.py:706
+msgid "Bed Booking Fee"
+msgstr "Betten-Buchungsgebühr"
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+#, fuzzy
+msgid "Transcript Fee"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/interfaces.py:724
+#, fuzzy
+msgid "Clearance enabled"
+msgstr "Einschreibung gestartet"
+
+#: waeup/kofa/interfaces.py:729
+#, fuzzy
+msgid "Payment disabled"
+msgstr "Zahlung durchgeführt"
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr "Einschreibung begonnen"
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr "eingeschrieben"
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr "Gebühren bezahlt"
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr "Missbrauch"
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr "Kein Mandat."
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr "Nichts zu tun."
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr "Falsche Mandatsparameter."
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr "Mandat abgelaufen."
+
+#: waeup/kofa/mandates/mandate.py:72
+#, fuzzy
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+"Das Passwort wurde erfolgreich gesetzt. Bitte gehen Sie zu Login-Seite."
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr "von"
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr "Aktivierungs-Code"
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr "Response Amount Approved"
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr "Response-Code"
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr "Response-Beschreibung"
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr "Noch nicht bezahlt"
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr "Bezahlt"
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr "Fehler"
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr "Kategorie der Zahlung"
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr "Gegenstand der Zahlung"
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr "Session der Zahlung"
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr "Status der Zahlung"
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr "Datum der Zahlung"
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr "Genehmigter Betrag"
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr "Zahlung bestätigt durch"
+
+#: waeup/kofa/reports.py:175
+#, fuzzy
+msgid "Unnamed Report"
+msgstr "Unbenanntes Zertifikat"
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr "System"
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr "Level wurde hinzugefügt."
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr "Dieses Level existiert."
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr "Kein Level ausgewählt."
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+#, fuzzy
+msgid "Request transcript"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:1054
+#, fuzzy
+msgid "Submit"
+msgstr "Endgültig einreichen"
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr "Falscher Status"
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr "entwertet"
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr "Sie sind nicht der Eigentümer dieses Zugangscodes."
+
+#: waeup/kofa/students/browser.py:1112
+#, fuzzy
+msgid "Transcript processing has been started."
+msgstr "Einschreibung wurde gestartet"
+
+#: waeup/kofa/students/browser.py:1124
+#, fuzzy
+msgid "Process transcript request"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/students/browser.py:1125
+#, fuzzy
+msgid "Save comment and mark as processed"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+#, fuzzy
+msgid "Transcript processed"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+#, fuzzy
+msgid "You are not allowed to view the transcript."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+#, fuzzy
+msgid "${a}: Transcript Data"
+msgstr "${a}: Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:1223
+#, fuzzy
+msgid "You are not allowed to download the transcript."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+#, fuzzy
+msgid "Academic Transcript"
+msgstr "Akademie"
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr "Credits"
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr "Score"
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+#, fuzzy
+msgid "Transfer student"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/browser.py:1292
+#, fuzzy
+msgid "Current level does not match certificate levels."
+msgstr "Das Studienjaht ist "
+
+#: waeup/kofa/students/browser.py:1295
+#, fuzzy
+msgid "Former study course record incomplete."
+msgstr "Die Studiengangdaten sind unvollständig."
+
+#: waeup/kofa/students/browser.py:1298
+#, fuzzy
+msgid "Maximum number of transfers exceeded."
+msgstr "Maximale Anzahl der Credit-Punkte von ${a} überschritten."
+
+#: waeup/kofa/students/browser.py:1301
+#, fuzzy
+msgid "Successfully transferred."
+msgstr "Erfolgreich hinzugefügt: ${a}"
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+#, fuzzy
+msgid "Revert now"
+msgstr "Jetzt beginnen"
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr "${a}: Level ${b}"
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr "Leveldaten"
+
+#: waeup/kofa/students/browser.py:1383
+msgid "Course Registration Slip"
+msgstr "Kursregistrierungsbeleg"
+
+#: waeup/kofa/students/browser.py:1391
+#, fuzzy
+msgid "1st Semester Courses"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/students/browser.py:1393
+#, fuzzy
+msgid "2nd Semester Courses"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/students/browser.py:1395
+#, fuzzy
+msgid "Level Courses"
+msgstr "Level-Kürzel"
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr "Dept."
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr "Fakultät"
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr "Cred."
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr "Kursticket hinzufügen"
+
+#: waeup/kofa/students/browser.py:1451
+#, fuzzy
+msgid "Enter valid course code"
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+#, fuzzy
+msgid "Course not found."
+msgstr "Keinen Studenten gefunden."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr "Verwalte Level  ${a}"
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr ""
+"Dieses Level stimmt nicht mit dem aktuellen Studienjahr des Studenten "
+"überein."
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr "Student hat den falschen Status."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+#, fuzzy
+msgid "Total credits exceed ${a}."
+msgstr "Gesamte Credits"
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr "Kursregistrierung wurde annulliert."
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr "Kursregistrierung wurde abgelehnt."
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr "Das Ticket existiert."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr "${a}: Kursticket ${b}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr "Erfolgreich hinzugefügt: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr "Verwalte Kursticket ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr "${a}: Zahlungen"
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr "Ticket erzeugen"
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr ""
+"Sie sind noch nicht in der Session angekommen, für die aktuell die "
+"Bettenbuchung aktiviert ist."
+
+#: waeup/kofa/students/browser.py:1742
+#, fuzzy
+msgid "You have not yet booked accommodation."
+msgstr "Unterkunft buchen"
+
+#: waeup/kofa/students/browser.py:1768
+#, fuzzy
+msgid "Add previous session online payment"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+#, fuzzy
+msgid "Add balance"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr "Studenten"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr " ${a}: Unterkunft"
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr "Bettticket hinzufügen"
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr "Bettticket erzeugen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr "Außerhalb der Antragsperiode: ${a} - ${b}"
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr "Außerhalb der Antragsperiode."
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr "Die Daten sind unvollständig."
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr "Sie haben den falschen Status."
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr "Sie haben in der aktuellen Session bereits ein Bett gebucht."
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr "In der Kategorie ${a} ist kein Bett mehr frei."
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:210
+#, fuzzy
+msgid "Previous Study Course"
+msgstr "Studiengang"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr "${a}, Block ${b}, Raum ${c}, Bett ${d} (${e})"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr "Bettticket erzeugt und Bett gebucht: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr "Bett-Ticket für Session ${a}"
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr "Bettenzuweisungsdaten"
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr "Zahlungen"
+
+#: waeup/kofa/students/browser.py:2161
+#, fuzzy
+msgid "Bed Allocation Slip"
+msgstr "Bettenzuweisung"
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr "Studenten in reservierten Betten können nicht umgezogen werden."
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr "Studen konnte nicht umgezogen werden."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr "Student umgezogen: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr "${a}: Historie"
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr "Basisdaten editieren"
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr "Passwort geändert"
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr "Unterkunft"
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr "Portrait hochladen"
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr "Einschreibung starten"
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr "Einschreibung jetzt starten"
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr "Es wurde kein Passbild hochgeladen."
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr "Es wurden nicht alle Pflichtfelder ausgefüllt."
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr "Einschreibung wurde gestartet"
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr "Bearbeite Einschreibungsdaten"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr "Bett-Ticket ${a}"
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr "Einschreibungsdaten wurden gespeichert."
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+"Dieses Formular kann nicht abgesendet werden, da Sie den falschen "
+"Antragsstatus haben."
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr "Einschreibung wurde beantragt."
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+"Geben Sie den CLR-Zugangscode ein, den Sie zum Starten der Einschreibung "
+"verwendet haben."
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr "Dies ist nicht Ihr CLR-Zugangscode"
+
+#: waeup/kofa/students/browser.py:2491
+msgid "Start session"
+msgstr "Beginne Studienjahr"
+
+#: waeup/kofa/students/browser.py:2495
+msgid "Start now"
+msgstr "Jetzt beginnen"
+
+#: waeup/kofa/students/browser.py:2504
+msgid "You are not entitled to start session."
+msgstr "Sie dürfen das Studienjahr noch nicht beginnen."
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+msgid "Session started."
+msgstr "Studienjahr begonnen."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr "Aktuelles Level ${a} hinzufügen"
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+#, fuzzy
+msgid "Your data are incomplete"
+msgstr "Die Daten sind unvollständig."
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr "Kursliste jetzt erzeugen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+msgid "Edit course list of ${a}"
+msgstr "${a} Kursliste bearbeiten"
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+"Sie sind ein graduierter Student, ihre Kursliste kann nicht registriert "
+"werden."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr "Maximale Anzahl der Credit-Punkte von ${a} überschritten."
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr "Kurseliste wurde registriert."
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr "Registriere Kursliste"
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr "Passwort setzen"
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr "Setzen"
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr "Mehr als einen Studenten gefunden."
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr "Passwort wurde bereits gesetzt. Ihre Studenten Id ist ${a}"
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+"Passwort wurde bereits gesetzt. Sie verwenden den falschen Aktiwierungs-Code."
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr "Passwort wurde gesetzt. Ihre Studenten Id ist ${a}"
+
+#: waeup/kofa/students/browser.py:2793
+msgid "Request password for first-time login"
+msgstr "Passwort zum erstmaligen Einloggen beantragen"
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+msgid "No student record found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr "Nur Jafreszahlen erlaubt (z.B. 2011)"
+
+#: waeup/kofa/students/browser.py:2882
+msgid "Your password request was successful."
+msgstr "Ihre Passwortanfrage war erfolgreich."
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr "Aktuelle Session"
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr "Aktuelles Level"
+
+#: waeup/kofa/students/browser.py:2949
+#, fuzzy
+msgid "in any session"
+msgstr "in der Session"
+
+#: waeup/kofa/students/browser.py:2955
+#, fuzzy
+msgid "at any level"
+msgstr "Einstiegslevel"
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr "Level"
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr "Studenten verwalten"
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+#, fuzzy
+msgid "Current academic session not set."
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+#, fuzzy
+msgid "Configure student data export"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/students/browser.py:3131
+#, fuzzy
+msgid "All Sessions"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:3138
+#, fuzzy
+msgid "All Levels"
+msgstr "Level"
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr "Student hinzufügen"
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr "Student erzeugt."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr "Setze temporäres Passwort für ${a}"
+
+#: waeup/kofa/students/browser.py:405
+msgid "Login now"
+msgstr "Jetzt einloggen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+#, fuzzy
+msgid "Login as ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/kofa/students/browser.py:415
+#, fuzzy
+msgid "You successfully logged in as student."
+msgstr "Sie haben erfolgreich Ihr Passwort geändert für das"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr "${a}: Basisdaten (Konto gesperrt)"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr "${a}: Basisdaten"
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr "Admission Letter von"
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr "Basisdaten verwalten"
+
+#: waeup/kofa/students/browser.py:572
+#, fuzzy
+msgid "Trigger registration transition"
+msgstr "Zustandsänderung der Registrierung:"
+
+#: waeup/kofa/students/browser.py:612
+#, fuzzy
+msgid "Student account has been activated."
+msgstr "Student wurde eingeschrieben."
+
+#: waeup/kofa/students/browser.py:631
+#, fuzzy
+msgid "Student account has been deactivated."
+msgstr "Student wurde eingeschrieben."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr "${a}: Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr "Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:702
+#, fuzzy
+msgid "Clearance Slip of"
+msgstr "Einschreibungsbeleg für"
+
+#: waeup/kofa/students/browser.py:711
+#, fuzzy
+msgid "Student Signature"
+msgstr "Studenten"
+
+#: waeup/kofa/students/browser.py:712
+#, fuzzy
+msgid "Clearance Officer Signature"
+msgstr "Einschreibungsgebühr"
+
+#: waeup/kofa/students/browser.py:719
+#, fuzzy
+msgid "Date, Student Signature"
+msgstr "Studenten"
+
+#: waeup/kofa/students/browser.py:720
+#, fuzzy
+msgid "Date, Clearance Officer Signature"
+msgstr "Einschreibungsgebühr"
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr "Verwalte Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr "Student wurde eingeschrieben."
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr "Student hat den falschen Status."
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr "Einschreibung zurückweisen"
+
+#: waeup/kofa/students/browser.py:802
+#, fuzzy
+msgid "Save comment and reject clearance now"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr "Einschreibung wurde annulliert."
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr "Einschreibung wurde abgelehnt."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr "${a}: Persönliche Daten"
+
+#: waeup/kofa/students/browser.py:856
+msgid "Manage personal data"
+msgstr "Persönliche Daten verwalten"
+
+#: waeup/kofa/students/browser.py:871
+msgid "Edit personal data"
+msgstr "Persönliche Daten bearbeiten"
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr "${a}: Studiengang"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+#, fuzzy
+msgid "${a}: Previous Study Course"
+msgstr "${a}: Studiengang"
+
+#: waeup/kofa/students/browser.py:932
+#, fuzzy
+msgid "First Study Course, "
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:934
+#, fuzzy
+msgid "Second Study Course"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/browser.py:939
+#, fuzzy
+msgid "First Study Course"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr "Studiengang verwalten"
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr "Ausgewählte Levels  löschen"
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr "Level  hinzufügen"
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr "Das Studienjaht ist "
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr "Bettkoordinaten"
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr "Betten-Kategorie"
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr "Wiederhole Passwort:"
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr "Scans"
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr "Alle auswählen"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr "mit Name"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr "mit Regsistrierungsnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr "mit Matrikelnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr "im Department"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr "im Studienmodus"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr "Suchergebnisse"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr "Status"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr "mit Id"
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr "Status"
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+#, fuzzy
+msgid "Course Level"
+msgstr "Aktuelles Level"
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+#, fuzzy
+msgid "Matric No."
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr "Vollständiger Name"
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+#, fuzzy
+msgid "Exporter (Data Type)"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+#, fuzzy
+msgid "Current Study Mode"
+msgstr "Studienmodus:"
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+#, fuzzy
+msgid "Export Number"
+msgstr "Nummer"
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+#, fuzzy
+msgid "Exporter"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr "Erzeuger"
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+#, fuzzy
+msgid "and"
+msgstr "Pflicht"
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+"Kofa erzeugt automatisch ein temporäres Passwort, welches das originale "
+"Passwort des Studenten ersetzt. Das Passwort bleibt für ${minutes} Minuten "
+"gültig. Während dieser Zeit kann sich der Student nicht einloggen. Nach "
+"Verfall des temporären Passworts kann das originale Passwort wieder "
+"verwendet werden. "
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+"Das temporäre Passwort \"${password}\" wurde gesetzt. Sie können sich durch "
+"Anklicken des Buttons unten jetzt einloggen."
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr "Kategorie"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr "Gegenstand"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr "Zahlungs-Id"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+"Wenn Ihr Datensatz gefunden wurde, werden die Zugangsdaten an die zur "
+"Verfügung gestellte Emailadresse versendet. Um sich einloggen zu können, "
+"müssen Sie somit eine gültige Email-Adresse besitzen!"
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+"Bitte helfen Sie uns, Missbrauch des Dienstes zu vermeiden, indem Sie das "
+"folgende Captcha lösen:"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+"Email konnte nicht versendet werden. Bitte versuchen Sie es später noch "
+"einmal."
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr "Vielen Dank, dass Sie Kofa verwendet haben!"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr "Ihre Zugangsdaten sind:"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr "Benutzername:"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr "Passwort:"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+"EIne Email mit Ihren Benutzernamen und Ihrem Passwort wurde versendet an"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr "Drucken Sie diese Seite und gehen Sie zu "
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+"Bitte achten Sie auf Groß- und Kleinschreibung bei Passwörtern und halten "
+"Sie Ihr Passwort geheim."
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr "Bitte prüfen Sie den Eingang dieser Email und fahren Sie dann fort."
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr "Zugangscode:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr "Anleitung:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+"<li> Erwerben Sie einen neuen Passwort-Zugangscode (PWD). </li> <li> Tragen "
+"Sie Ihre Registrierungsnummer und den Zugangscode ein. </li> <li> Nachdem "
+"Sie auf den Setzen-Button geklickt haben, wird die Studenten-Id angezeigt "
+"und das Passwort gesetzt. Die zehnstellige Zahl des soeben eingegebenen "
+"Zugangscodes ist Ihr neues Passwort.</li> <li> Gehen Sie zur <strong><a href="
+"\"login\">allgemeinen Login-Seite</a></strong> und geben Sie Ihre neuen "
+"Zugangsdaten ein. </li>"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr "Regsistrierungsnummer:"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr "Levels"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr "Level-Kürzel"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr "Level-Titel"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr "in der Session"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+#, fuzzy
+msgid "Previous Study Courses:"
+msgstr "Gewünschter Studiengang"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr "Levels (Kurslisten)"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr "Es existieren bisher keine Levels registriert."
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr "Text zur Erklärung der Kursregistrierung."
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr "CO"
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+#, fuzzy
+msgid "Enter valid course code."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr "Kurstickets (Gesamte Credits: ${total_credits})"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr "Fkt."
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr "Level-Daten"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr "Dep."
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr "Fak."
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr "Erford."
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr "Auto"
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr "Bisher wurden keine Tickets hinzugefügt."
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+#, fuzzy
+msgid "Subjects/Courses"
+msgstr "Kurseregistrierung zurückweisen"
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+#, fuzzy
+msgid "Process request"
+msgstr "Prozessorname"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+#, fuzzy
+msgid "transcript_request"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+#, fuzzy
+msgid "Dispatch Address:"
+msgstr "Emailadresse:"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr "Aktivierungs-Code:"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+#, fuzzy
+msgid "Send new request"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/students/export.py:255
+msgid "Student Study Courses"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/export.py:285
+msgid "Student Study Levels"
+msgstr "Levels"
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr "Kurstickets"
+
+#: waeup/kofa/students/export.py:345
+msgid "Student Payments"
+msgstr "Student Bezahlungen"
+
+#: waeup/kofa/students/export.py:382
+#, fuzzy
+msgid "Payment Data for Bursary"
+msgstr "Zahlung"
+
+#: waeup/kofa/students/export.py:413
+#, fuzzy
+msgid "Bed Tickets"
+msgstr "Bett-Ticket ${a}"
+
+#: waeup/kofa/students/export.py:449
+#, fuzzy
+msgid "Student Payments Overview"
+msgstr "Student Bezahlungen"
+
+#: waeup/kofa/students/export.py:482
+#, fuzzy
+msgid "Student Study Levels Overview"
+msgstr "Levels"
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+#, fuzzy
+msgid "Reasons for Deactivation"
+msgstr "Response-Beschreibung"
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr "PWD-Aktivierungs-Code"
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr "Emailadresse"
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr "Einschreibungsformular gesperrt"
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr "CLR-Aktivierungs-Code"
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr "Nationalität"
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr "Arbeitgeber"
+
+#: waeup/kofa/students/interfaces.py:343
+#, fuzzy
+msgid "Updated"
+msgstr "Alle Betten aktualisieren"
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr "Postanschrift"
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+msgid "Registr. or Matric. Number"
+msgstr "Registr.- oder Matrikelnummer"
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr "Zertifikat"
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr "Studienmodus bei Einschreibung"
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr "Eingangssession"
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr "Aktuelles Verdict"
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr "Vorangegangenes Verdict"
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+#, fuzzy
+msgid "Validated by"
+msgstr "entwertet"
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr "Verdict"
+
+#: waeup/kofa/students/interfaces.py:590
+#, fuzzy
+msgid "Validation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/students/interfaces.py:596
+#, fuzzy
+msgid "Total Credits"
+msgstr "Gesamte Credits"
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+#, fuzzy
+msgid "Faculty Code"
+msgstr "Fakultät"
+
+#: waeup/kofa/students/interfaces.py:639
+#, fuzzy
+msgid "Department Code"
+msgstr "Department"
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr "Semester/Term"
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr "Passmark"
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr "Erforderlich"
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr "Kurs aus vergangenem Studienjahr"
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr "Automatische Erzeugung"
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr "Kurs"
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+#, fuzzy
+msgid "Level Session"
+msgstr "Session"
+
+#: waeup/kofa/students/interfaces.py:735
+#, fuzzy
+msgid "Allocated Bed"
+msgstr "Student zuweisen"
+
+#: waeup/kofa/students/interfaces.py:741
+#, fuzzy
+msgid "Requested Bed Type"
+msgstr "Betttyp"
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr "Buchungsdatum"
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr "Aktivierungs-Code der Buchung"
+
+#: waeup/kofa/students/interfaces.py:781
+#, fuzzy
+msgid "Current Session Payment"
+msgstr "Aktuelle Session"
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+msgid "Payment Level"
+msgstr "Studienjahr bei Zahlung"
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+#, fuzzy
+msgid "Payment approved."
+msgstr "Zahlung bestätigt"
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr "Department"
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+#, fuzzy
+msgid "Study Modes Group"
+msgstr "Studienmodus:"
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+#, fuzzy
+msgid "Level Report"
+msgstr "Level-Kürzel"
+
+#: waeup/kofa/students/reports/level_report.py:218
+#, fuzzy
+msgid "Create level report"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+#, fuzzy
+msgid "Session Results Presentation"
+msgstr "Session-Konfiguration"
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+#, fuzzy
+msgid "Student Statistics"
+msgstr "Containerstatistik"
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+#, fuzzy
+msgid "Create student statistics report"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/students/utils.py:126
+#, fuzzy
+msgid "Name:"
+msgstr "Name"
+
+#: waeup/kofa/students/utils.py:145
+#, fuzzy
+msgid "Study Course:"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr "Department:"
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr "Fakultät:"
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr "Studienmodus:"
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+#, fuzzy
+msgid "Entry Session:"
+msgstr "Eingangssession"
+
+#: waeup/kofa/students/utils.py:187
+#, fuzzy
+msgid "Current Level:"
+msgstr "Aktuelles Level"
+
+#: waeup/kofa/students/utils.py:197
+#, fuzzy
+msgid "Date of Birth:"
+msgstr "Geburtsdatum"
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr "Gescannte Dokumente"
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+msgid "Study course data are incomplete."
+msgstr "Die Studiengangdaten sind unvollständig."
+
+#: waeup/kofa/students/utils.py:470
+#, fuzzy
+msgid "no bed allocated"
+msgstr "Unterkunftbeleg herunterladen"
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+#, fuzzy
+msgid "Amount must be greater than 0."
+msgstr "Betrag konnte nicht ermittelt werden."
+
+#: waeup/kofa/students/utils.py:521
+#, fuzzy
+msgid "Matriculation number cannot be set."
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/utils.py:543
+#, fuzzy
+msgid "Matriculation number already set."
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/utils.py:545
+#, fuzzy
+msgid "No certificate assigned."
+msgstr "Zertifikat-Kurs hinzufügen"
+
+#: waeup/kofa/students/utils.py:552
+#, fuzzy
+msgid "Matriculation number exists."
+msgstr "Matrikelnummer"
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+#, fuzzy
+msgid "Your Kofa student record was created on ${a}."
+msgstr "Student erzeugt."
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr "Basisdaten"
+
+#: waeup/kofa/students/utils.py:720
+#, fuzzy
+msgid "Workflow History"
+msgstr "Workflow-Status"
+
+#: waeup/kofa/students/utils.py:820
+#, fuzzy
+msgid "Course of Study:"
+msgstr "Zugelassen für Studienfach:"
+
+#: waeup/kofa/students/utils.py:847
+#, fuzzy
+msgid "Entry Mode:"
+msgstr "Studienmodus bei Einschreibung"
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr "(noch nicht zugewiesen)"
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr "Persönliche Daten"
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr "Historie"
+
+#: waeup/kofa/students/viewlets.py:185
+#, fuzzy
+msgid "Trigger transition"
+msgstr "Keine Änderung"
+
+#: waeup/kofa/students/viewlets.py:194
+#, fuzzy
+msgid "Login as student"
+msgstr "nach Studenten"
+
+#: waeup/kofa/students/viewlets.py:203
+#, fuzzy
+msgid "Download admission letter"
+msgstr "Antrag herunterladen"
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr "Deaktiviere Konto"
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr "Aktiviere Konto"
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr "Student einschreiben"
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr "Einschreibungsbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr "Ansehen"
+
+#: waeup/kofa/students/viewlets.py:359
+#, fuzzy
+msgid "Transcript"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr "Kurse validieren"
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr "Kurseregistrierung zurückweisen"
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr "Kurseregistrierungsbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr "Unterkunft buchen"
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr "Unterkunftbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr "Student umziehen"
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr "Porträt ändern"
+
+#: waeup/kofa/students/viewlets.py:607
+msgid "Start new session"
+msgstr "Beginne neues Studienjahr"
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr "Kursliste hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:640
+msgid "Edit course list"
+msgstr "Kursliste bearbeiten"
+
+#: waeup/kofa/students/viewlets.py:658
+#, fuzzy
+msgid "Add current session payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:667
+#, fuzzy
+msgid "Add previous session payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:683
+#, fuzzy
+msgid "Add balance payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr "Unterkunft"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr "${a} gelöscht."
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/kofa/students/viewlets.py:839
+msgid "Could not determine file type."
+msgstr "Dateityp konnte nicht ermittelt werden."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr "${a} Dateiendung erwartet."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+msgid "Only the following extensions are allowed: ${a}"
+msgstr "Nur die folgenden Erweiterungen sind erlaubt: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr "Datei"
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/kofa/students/viewlets.py:902
+#, fuzzy
+msgid "Upload selected file"
+msgstr "Neue Datei hochladen"
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr "Antrag"
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr "Keine lokale Datei ausgewählt."
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr "Passbild"
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr "Passbild (nur jpg)"
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr "Geburtsurkunde"
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr "Geburtsurkunde-Scan"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr "${title} / 2. Nachholen"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr "${title} / 3. Nachholen"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr "${title} / 1. Nachholen"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr "${title} / 1. Wiederholung"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr "${title} / 2. Wiederholung"
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr "männlich"
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr "weiblich"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+msgid "Error: wrong level id ${value}"
+msgstr "Fehler: falsche level_id ${value}"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr "Fehler: level_id ${value} nicht verfügbar"
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr "Auf 'eingeschrieben' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:105
+msgid "Reset to 'cleared'"
+msgstr "Auf 'eingeschrieben' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:112
+#, fuzzy
+msgid "School fee payment made"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/students/workflow.py:118
+#, fuzzy
+msgid "Pay PG school fee"
+msgstr "Gebühren bezahlen"
+
+#: waeup/kofa/students/workflow.py:119
+#, fuzzy
+msgid "PG school fee payment made"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/students/workflow.py:125
+#, fuzzy
+msgid "Approve school fee payment"
+msgstr "Zahlung bestätigen"
+
+#: waeup/kofa/students/workflow.py:126
+msgid "School fee payment approved"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/students/workflow.py:132
+#, fuzzy
+msgid "Approve PG school fee payment"
+msgstr "Zahlung bestätigen"
+
+#: waeup/kofa/students/workflow.py:133
+#, fuzzy
+msgid "PG school fee payment approved"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr "Setze zurück auf 'rückgemeldet'"
+
+#: waeup/kofa/students/workflow.py:140
+msgid "Reset to 'returning'"
+msgstr "Auf 'rückgemeldet' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr "Kurse registriert"
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+#, fuzzy
+msgid "Reset to school fee paid"
+msgstr "Gebühren bezahlt"
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+#, fuzzy
+msgid "Reset to 'school fee paid'"
+msgstr "Gebühren bezahlt"
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr "Kurse validiert"
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+#, fuzzy
+msgid "Course validation bypassed"
+msgstr "Kurse validiert"
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr "Zurückmelden"
+
+#: waeup/kofa/students/workflow.py:182
+#, fuzzy
+msgid "Returned"
+msgstr "Zurückmelden"
+
+#: waeup/kofa/students/workflow.py:188
+#, fuzzy
+msgid "Reset to courses validated"
+msgstr "Auf 'Kurse validiert' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:189
+#, fuzzy
+msgid "Reset to 'courses validated'"
+msgstr "Auf 'Kurse validiert' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:197
+#, fuzzy
+msgid "Request transript"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/workflow.py:198
+#, fuzzy
+msgid "Transcript requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/kofa/students/workflow.py:29
+#, fuzzy
+msgid "Record created"
+msgstr "Student erzeugt"
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr "Student zulassen"
+
+#: waeup/kofa/students/workflow.py:35
+#, fuzzy
+msgid "Admitted"
+msgstr "zugelassen"
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr "Zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:42
+msgid "Reset to initial state"
+msgstr "Auf 'initialisiert' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr "Einschreibung gestartet"
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr "Auf 'zugelassen' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:56
+msgid "Reset to 'admitted'"
+msgstr "Auf 'zugelassen' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+#, fuzzy
+msgid "Reset to clearance started"
+msgstr "Auf 'eingeschrieben' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+#, fuzzy
+msgid "Reset to 'clearance started'"
+msgstr "Auf 'Einschreibung begonnen' zurücksetzen"
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr "eingeschrieben"
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr "Gebühren bezahlen"
+
+#: waeup/kofa/students/workflow.py:91
+#, fuzzy
+msgid "First school fee payment made"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/students/workflow.py:98
+#, fuzzy
+msgid "First school fee payment approved"
+msgstr "Bezahlung der Schulgebühr bestätigt"
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr "Kurse"
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr "Zertifikate"
+
+#: waeup/kofa/university/export.py:191
+msgid "Courses in Certificates"
+msgstr "Kurse in Zertifikaten"
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr "Titel des Kurses"
+
+#: waeup/kofa/university/interfaces.py:142
+#, fuzzy
+msgid "Former course"
+msgstr "Cert.-Kurs"
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr "Studienmodus"
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr "Einstiegslevel"
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr "Abschlusslevel"
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr "Antragskategorie"
+
+#: waeup/kofa/university/interfaces.py:195
+msgid "Initial School Fee"
+msgstr "Schulgebühr bei Einschreibung"
+
+#: waeup/kofa/university/interfaces.py:200
+msgid "Returning School Fee"
+msgstr "Schulgebühr bei Rückmeldung"
+
+#: waeup/kofa/university/interfaces.py:205
+#, fuzzy
+msgid "Foreigner Initial School Fee"
+msgstr "Schulgebühr bei Einschreibung"
+
+#: waeup/kofa/university/interfaces.py:210
+#, fuzzy
+msgid "Foreigner Returning School Fee"
+msgstr "Schulgebühr bei Rückmeldung"
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr "Einstiegs- und Abschlusslevel müssen übereinstimmen."
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr "Studienmodus, Einstiegs- und Abschlusslevel müssen korrespondieren."
+
+#: waeup/kofa/university/interfaces.py:281
+#, fuzzy
+msgid "Registration required"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr "Name der Fakultät"
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr "Prefix"
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr "Name des Departments"
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr "Vorstudium"
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr "1. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr "2. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr "3. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr "4. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr "5. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr "6. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr "7. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr "8. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "900 (Year 9)"
+msgstr "9. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "Postgraduate Level"
+msgstr "Postgraduierten-Level"
+
+#: waeup/kofa/userscontainer.py:79
+msgid "Users"
+msgstr "Benutzer"
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr "Albanien"
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr "Guinea"
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr "Guinea-Bissau"
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr "Guyana"
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr "Haiti"
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr "Heard Island and Mcdonald Islands"
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr "Vatikan"
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr "Honduras"
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr "Hong Kong"
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr "Ungarn"
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr "Island"
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr "Algerien"
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr "Indien"
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr "Indonesien"
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr "Iran"
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr "Irak"
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr "Irland"
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr "Isle of Man"
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr "Israel"
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr "Italien"
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr "Jamaica"
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr "Japan"
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr "American Samoa"
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr "Jersey"
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr "Jordanien"
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr "Kazakhstan"
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr "Kenia"
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr "Kiribati"
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr "Korea, Democratic People's Republic of"
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr "Korea, Republic of"
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr "Kuwait"
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr "Kyrgyzstan"
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr "Lao People's Democratic Republic"
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr "Andorra"
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr "Lettland"
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr "Libanon"
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr "Lesotho"
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr "Liberia"
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr "Libyen"
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr "Liechtenstein"
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr "Littauen"
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr "Luxemburg"
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr "Macao"
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr "Mazedonien"
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr "Angola"
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr "Madagascar"
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr "Malawi"
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr "Malaysia"
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr "Malediven"
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr "Mali"
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr "Malta"
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr "Marshall-Inseln"
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr "Martinique"
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr "Mauretanien"
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr "Mauritius"
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr "Anguilla"
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr "Mayotte"
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr "Mexiko"
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr "Micronesia, Federated States of"
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr "Moldavien"
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr "Monaco"
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr "Mongolei"
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr "Montenegro"
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr "Montserrat"
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr "Marokko"
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr "Mosambik"
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr "Antarktis"
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr "Myanmar"
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr "Namibia"
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr "Nauru"
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr "Nepal"
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr "Niederlande"
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr "New Caledonia"
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr "Neuseeland"
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr "Nicaragua"
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr "Niger"
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+msgid "Nigeria"
+msgstr "Nigeria"
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr "Antigua and Barbuda"
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr "Niue"
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr "Norfolk Island"
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr "Northern Mariana Islands"
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr "Norwegen"
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr "Oman"
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr "Pakistan"
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr "Palau"
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr "Palestina"
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr "Panama"
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr "Papua New Guinea"
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr "Argentinien"
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr "Paraguay"
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr "Peru"
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr "Philippinen"
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr "Pitcairn"
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr "Polen"
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr "Portugal"
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr "Puerto Rico"
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr "Qatar"
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr "Réunion"
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr "Rumänien"
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr "Armenien"
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr "Russland"
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr "Ruanda"
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr "Saint Barthélemy"
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr "Saint Helena, Ascension and Tristan Da Cunha"
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr "Saint Kitts and Nevis"
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr "Saint Lucia"
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr "Saint Martin (French Part)"
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr "Saint Pierre and Miquelon"
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr "Saint Vincent and the Grenadines"
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr "Samoa"
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr "Aruba"
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr "San Marino"
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr "Sao Tome and Principe"
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr "Saudi-Arabien"
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr "Senegal"
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr "Serbien"
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr "Seyschellen"
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr "Sierra Leone"
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr "Singapur"
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr "Sint Maarten (Niederlande)"
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr "Slowakei"
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr "Australien"
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr "Slovenien"
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr "Solomon Islands"
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr "Somalia"
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr "Südafrika"
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr "South Georgia and the South Sandwich Islands"
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr "Südsudan"
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr "Spanien"
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr "Sri Lanka"
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr "Sudan"
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr "Suriname"
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr "Österreich"
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr "Svalbard and Jan Mayen"
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr "Swaziland"
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr "Schweden"
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr "Schweiz"
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr "Syrien"
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr "Taiwan"
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr "Tajikistan"
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr "Tansania"
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr "Thailand"
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr "Ost-Timor"
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr "Azerbaijan"
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr "Togo"
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr "Tokelau"
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr "Tonga"
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr "Trinidad and Tobago"
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr "Tunesien"
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr "Türkei"
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr "Turkmenistan"
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr "Turks and Caicos Islands"
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr "Tuvalu"
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr "Uganda"
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr "Bahamas"
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr "Ukraine"
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr "Vereinigte Arabische Emirate"
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr "Vereinigtes Königreich"
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr "Vereinigte Staaten von Amerika"
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr "United States Minor Outlying Islands"
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr "Uruguay"
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr "Usbekistan"
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr "Vanuatu"
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr "Venezuela, Bolivarian Republic of"
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr "Vietnam"
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr "Bahrain"
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr "Virgin Islands, British"
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr "Virgin Islands, U.S."
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr "Wallis and Futuna"
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr "Westsahara"
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr "Jemen"
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr "Sambia"
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr "Simbabwe"
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr "Bangladesh"
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr "Barbados"
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr "Weißrussland"
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr "Belgien"
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr "Belize"
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr "Benin"
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr "Bermuda"
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr "Bhutan"
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr "Bolivien"
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr "Bonaire, Sint Eustatius and Saba"
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr "Bosnia and Herzegovina"
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr "Botswana"
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr "Bouvet Island"
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr "Brasilien"
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr "British Indian Ocean Territory"
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr "Brunei Darussalam"
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr "Bulgarien"
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr "Burkina Faso"
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr "Burundi"
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr "Cambodia"
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr "Kamerun"
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr "Kanada"
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr "Cape Verde"
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr "Cayman Islands"
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr "Zentralafrikanische Republik"
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr "Tschad"
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr "Chile"
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr "China"
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr "Christmas Island"
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr "Cocos (Keeling) Islands"
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr "Kolumbien"
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr "Komoren"
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr "Kongo"
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr "Kongo, Demokratische Republik"
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr "Cook Islands"
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr "Costa Rica"
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr "Côte d'Ivoire (Elfenbeinküste)"
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr "Kroatien"
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr "Kuba"
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr "Curaçao"
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr "Zypern"
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr "Tschechische Republik"
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr "Dänemark"
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr "Djibouti"
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr "Dominica"
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr "Dominikanische Republik"
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr "Ecuador"
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr "Ägypten"
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr "El Salvador"
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr "Äquatorial-Guinea"
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr "Eritrea"
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr "Estland"
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr "Äthiopien"
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr "alklandinseln"
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr "Afghanistan"
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr "Faroe-Inseln"
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr "Fiji"
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr "Finnland"
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr "Frankreich"
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr "Französisch-Guiana"
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr "Französisch-Plynesien"
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr "French Southern Territories"
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr "Gabun"
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr "Gambia"
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr "Georgien"
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr "Åland Islands"
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr "Deutschland"
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr "Ghana"
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr "Gibraltar"
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr "Griechenland"
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr "Grönland"
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr "Grenada"
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr "Guadeloupe"
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr "Guam"
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr "Guatemala"
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr "Guernsey"
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+"Vollständiger Name: ${a}\n"
+"Benutzer-Id: ${b}\n"
+"Benutzertyp: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+"Hallo ${a},\n"
+"\n"
+"${b}\n"
+"Studenten-Informations- und Registrierungsportal der\n"
+"${c}.\n"
+"\n"
+"Ihr Benutzername: ${d}\n"
+"Ihr Passwort: ${e}\n"
+"${f}\n"
+"\n"
+"Bitte merken Sie sich den Benutzernamen und \n"
+"halten Sie Ihr Passwort geheim!\n"
+"\n"
+"Bitte achten Sie auf und Groß- und Kleinschreibung \n"
+"bei Passwörter und Benutzernamen.\n"
+"\n"
+"Mit freundlichen Grüßen,\n"
+"${c}\n"
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr "USA"
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr "Ungültiges Datetime-Format"
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr "Leere Feld."
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr "Internationale Vorwahl erwartet: '+NNN'"
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr "Telefonnummern dürfen nur Nummern enthalten."
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+"Vergessen Sie nicht, das Formular zu sichern, wenn Sie Elemente hinzugefügt "
+"oder entfernt haben."
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+msgid "remove-selected-items"
+msgstr "Ausgewählte Elemente löschen"
+
+#, fuzzy
+#~ msgid "Application Fee Payment Tickets"
+#~ msgstr "Zullassungsgebühr-Tickets"
+
+#~ msgid "Back"
+#~ msgstr "Zurück"
+
+#~ msgid "Error: ${a}"
+#~ msgstr "Fehler: ${a}"
+
+#, fuzzy
+#~ msgid "Reload page"
+#~ msgstr "Seite neu laden"
+
+#~ msgid "Overwrite contents in new location?"
+#~ msgstr "Existierende Dateien an neuem Ort überschreiben?"
+
+#~ msgid "Copy existing data to new storage?"
+#~ msgstr "Existierende Dateien an neuen Ort kopieren?"
+
+#~ msgid "Download"
+#~ msgstr "Herunterladen"
+
+#, fuzzy
+#~ msgid "Type of report to create"
+#~ msgstr "Zu exportierender Objekttyp"
+
+#~ msgid "Back to bottom"
+#~ msgstr "Zurück zum Ende"
+
+#~ msgid "Back to top"
+#~ msgstr "Zurück zum Anfang"
+
+#~ msgid "Kofa - Student Management System"
+#~ msgstr "Kofa - Student Management System"
+
+#, fuzzy
+#~ msgid "Copyright &copy; WAeUP Group 2008-2014"
+#~ msgstr "Copyright &copy; WAeUP Group 2008-2012"
+
+#~ msgid "Student Id:"
+#~ msgstr "Studenten Id:"
+
+#~ msgid "State:"
+#~ msgstr "Status:"
+
+#~ msgid "Skin"
+#~ msgstr "Skin"
+
+#~ msgid "Student Section"
+#~ msgstr "Studenten"
+
+#, fuzzy
+#~ msgid "Current Session:"
+#~ msgstr "Aktuelle Session"
+
+#, fuzzy
+#~ msgid "Level Session:"
+#~ msgstr "Session"
+
+#, fuzzy
+#~ msgid "Set department filter."
+#~ msgstr "im Studienmodus"
+
+#, fuzzy
+#~ msgid "Select level to report."
+#~ msgstr "Akademie durchsuchen"
+
+#, fuzzy
+#~ msgid "Set study modes filter."
+#~ msgstr "im Studienmodus"
+
+#, fuzzy
+#~ msgid "Select academic session to report."
+#~ msgstr "Akademie durchsuchen"
+
+#~ msgid "Delete attachment"
+#~ msgstr "Anhang löschen"
+
+#~ msgid "Search applicants"
+#~ msgstr "Suche nach Antragstellern"
+
+#~ msgid "Sem."
+#~ msgstr "Sem."
+
+#~ msgid "for students"
+#~ msgstr "nach Studenten"
+
+#~ msgid "Enter unique course code which will become part of the URL."
+#~ msgstr ""
+#~ "Geben Sie ein eindeutiges Kürzel ein, das dann Teil der URL sein wird."
+
+#~ msgid "Unnamed Certificate"
+#~ msgstr "Unbenanntes Zertifikat"
+
+#~ msgid "Enter unique certificate code which will become part of the URL."
+#~ msgstr ""
+#~ "Geben Sie ein eindeutiges Kürzel ein, das dann Teil der URL sein wird."
+
+#~ msgid "Unnamed"
+#~ msgstr "Unbenannt"
+
+#~ msgid "This code will become part of the URL."
+#~ msgstr "Dieses Kürzel wird Teil der URL sein."
+
+#~ msgid "View processing log"
+#~ msgstr "Logdatei betrachten"
+
+#~ msgid "Back to data center"
+#~ msgstr "Zurück zum Datenzentrum"
+
+#, fuzzy
+#~ msgid "This export was already discarded."
+#~ msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#~ msgid ""
+#~ "The file will be generated and then be made available for you to "
+#~ "download. You must download or discard any existing export file before "
+#~ "creating a new one."
+#~ msgstr ""
+#~ "Die Datei wird erzeugt und zum Herunterladen bereitgestellt. Sie müssen "
+#~ "eine existierende Export-Datei herunterladen, bevor sie eine neue Datei "
+#~ "erzeugen können."
+
+#, fuzzy
+#~ msgid "Here you can create CSV files from parts of portal data."
+#~ msgstr "Hier können Sie Daten aus dem Portal herunterladen."
+
+#~ msgid ""
+#~ "Please pick the type of objects you want to export from the selection "
+#~ "below."
+#~ msgstr ""
+#~ "Wählen Sie aus der Auswahlliste einen Datentyp aus, den Sie exportieren "
+#~ "möchten."
+
+#~ msgid "Form Status:"
+#~ msgstr "Formularstatus:"
+
+#~ msgid "Regular Hostel"
+#~ msgstr "Standard-Wohnheim"
+
+#~ msgid "Blocked Hostel"
+#~ msgstr "Gesperrtes Wohnheim"
+
+#~ msgid "Postgraduate Hostel"
+#~ msgstr "Postgraduierten-Wohnheim"
+
+#~ msgid "Payment Access Code"
+#~ msgstr "Zugangscode der Zahlung"
+
+#~ msgid "School Fee"
+#~ msgstr "Schulgebühr"
+
+#~ msgid "Bed Allocation Fee"
+#~ msgstr "Bettenzuweisungsgebühr"
+
+#~ msgid "Hostel Maintenance Fee"
+#~ msgstr "Wohnraum-Mietgebühr"
+
+#~ msgid "Transfer Fee"
+#~ msgstr "Transfer Fee"
+
+#~ msgid "Gown Hire Fee"
+#~ msgstr "Gown Hire Fee"
+
+#~ msgid "Course List"
+#~ msgstr "Kursliste"
+
+#~ msgid "Semester:"
+#~ msgstr "Semester:"
+
+#~ msgid "Credits:"
+#~ msgstr "Credits:"
+
+#~ msgid "Passmark:"
+#~ msgstr "Passmark:"
+
+#~ msgid "(not provided)"
+#~ msgstr "(nicht hochgeladen)"
+
+#, fuzzy
+#~ msgid ""
+#~ "This type of payment has already been made. Would you like to pay for a "
+#~ "previous session?"
+#~ msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#, fuzzy
+#~ msgid "${a} Upload"
+#~ msgstr "Hochladen"
+
+#~ msgid "Entries/(invalidated)"
+#~ msgstr "Einträge / (verbraucht)"
+
+#~ msgid "Is mandatory course (not elective):"
+#~ msgstr "Pflichtkurs:"
+
+#~ msgid "Mandatory"
+#~ msgstr "Pflicht"
+
+#~ msgid "Is mandatory course (not elective)"
+#~ msgstr "Pflichtkurs"
+
+#~ msgid "Get login credentials"
+#~ msgstr "Zugangsdaten zusenden"
+
+#~ msgid "Screening Venue"
+#~ msgstr "Screening-Ort"
+
+#~ msgid "Screening Score"
+#~ msgstr "Screening-Score"
+
+#~ msgid "Describe Score here."
+#~ msgstr "Describe Score here."
+
+#~ msgid "Upload file"
+#~ msgstr "Datei hochladen"
+
+#~ msgid "Batch processing"
+#~ msgstr "Stapelverarbeitung"
+
+#~ msgid "Upload CSV file"
+#~ msgstr "Datei hochladen"
+
+#~ msgid "Clearance"
+#~ msgstr "Einschreibung"
+
+#~ msgid "Bed Allocation"
+#~ msgstr "Bettenzuweisung"
+
+#~ msgid "Gown"
+#~ msgstr "Talar"
+
+#~ msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+#~ msgstr "<font size=10>${a}, Seite ${b} von ${c}</font>"
+
+#~ msgid "Acceptance Letter Scan"
+#~ msgstr "Acceptance Letter Scan"
+
+#~ msgid "Pay postgraduate school fee"
+#~ msgstr "Postgraduierten-Schulgebühr bezahlen"
+
+#~ msgid "Approve postgraduate payment"
+#~ msgstr "Postgraduierten-Zahlung bestätigen"
+
+#~ msgid "Reset to paid"
+#~ msgstr "Auf 'Gebühren bezahlt' zurücksetzen"
+
+#~ msgid "Reset to 'paid'"
+#~ msgstr "Auf 'Gebühren bezahlt' zurücksetzen"
+
+#~ msgid "Student admitted"
+#~ msgstr "Student zugelassen"
+
+#~ msgid "School fee paid"
+#~ msgstr "Gebühren bezahlt"
+
+#~ msgid "Add course referrer"
+#~ msgstr "Kursreferenz hinzufügen"
+
+#~ msgid "Remove selected course referrers"
+#~ msgstr "Ausgewählte Kursreferenzen löschen"
+
+#~ msgid "Edit course referrer"
+#~ msgstr "Kursreferenz bearbeiten"
+
+#~ msgid "Course Referrers"
+#~ msgstr "Kursreferenzen"
+
+#~ msgid "Referrer"
+#~ msgstr "Kursreferenz"
+
+#~ msgid "Add and remove course tickets of study level ${a}"
+#~ msgstr "Kurstickets des Levels  ${a} hinzufügen oder löschen"
+
+#~ msgid "Add and remove courses"
+#~ msgstr "Kurs hinzufügen oder löschen"
+
+#~ msgid "Course Referrer"
+#~ msgstr "Kursreferenz"
+
+#, fuzzy
+#~ msgid "Error: %s"
+#~ msgstr "Fehler: ${a}"
+
+#~ msgid "Application has not yet started."
+#~ msgstr "Antragszeitraum hat noch nicht begonnen."
+
+#~ msgid "Application has ended."
+#~ msgstr "Antragszeitraum ist beendet."
+
+#~ msgid "There no subobjects registered yet."
+#~ msgstr "Es existieren bisher keine Unterobjekte."
+
+#~ msgid "Accommodation Booking Session"
+#~ msgstr "Buchungssession"
+
+#~ msgid "Allowed States for Accommodation Booking"
+#~ msgstr "Erlaubte Zustände für das Buchen einer Unterbringung"
+
+#~ msgid "You can't re-upload pending data files."
+#~ msgstr "Sie können keine pending-Dateien hochladen."
+
+#~ msgid "Portal roles"
+#~ msgstr "Portalweite Rollen"
+
+#~ msgid "Start course registration"
+#~ msgstr "Kurseregistrierung starten"
+
+#~ msgid "Start course registration now"
+#~ msgstr "Kursregistrierung jetzt starten"
+
+#~ msgid "You are in wrong state."
+#~ msgstr "Sie sind im falschen Status."
+
+#~ msgid "Course registration has been started."
+#~ msgstr "Kursregistrierung wurde gestartet."
+
+#~ msgid "File name has no extension."
+#~ msgstr "Dateiname hat keine Endung."
+
+#~ msgid "File name contains more than one dot."
+#~ msgstr "Dateiname enthält mehr als einen Punkt."
+
+#~ msgid "Valid callback received."
+#~ msgstr "Gültigen Rückruf erhalten."
+
+#~ msgid "Fee paid"
+#~ msgstr "Gebühr bezahlt"
+
+#~ msgid "Valid callback received. ${a}"
+#~ msgstr "Gültigen Rückruf erhalten. ${a}"
+
+#~ msgid "Entry Level"
+#~ msgstr "Einstiegslevel"
+
+#~ msgid "Change your password when you have logged in."
+#~ msgstr "Ändern Sie Ihr Passwort, sobald Sie sich eingeloggt haben."
+
+#~ msgid "Get new login credentials"
+#~ msgstr "Zugangsdaten zusenden"
+
+#~ msgid "Application package not installed."
+#~ msgstr "Application package not installed."
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po	(revision 16045)
@@ -0,0 +1,58 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"PO-Revision-Date: 2012-04-13 10:21+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: de-DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: \n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr "--"
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr "--"
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr "--"
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr "ja"
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr "nein"
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,6397 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2013-06-27 09:46+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: en-US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: English\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Cancel Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:38
+msgid "AccessCodeBatches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:69
+msgid "AccessCodes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:134
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:137
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:103
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1120
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:122
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1222
+msgid "Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+msgid "Export started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:159
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:66
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:742
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:763
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Payment Tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr ""
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr ""
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:145
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:219
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:520
+msgid "Identification Number"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:137
+msgid "Manage container"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:147
+msgid "Export applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+msgid "Approve payment"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:144
+msgid "Application reset to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:157
+msgid "Reset application to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:158
+msgid "Application reset to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:66
+msgid "Pay application fee"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:67
+msgid "Payment made"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr ""
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1030
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1045
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1422
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1528
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+msgid "Given storage path cannot be used. ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1577
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1721
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1807
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1847
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1996
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2244
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2325
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2373
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:327
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:342
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:469
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:932
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:946
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:995
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+msgid "Required course:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr ""
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+msgid "Password"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+msgid "User Name or Id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:518
+msgid "Update scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:583
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:592
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:610
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:160
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:320
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:339
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:141
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:243
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:258
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+msgid "Hostel Allocation Start Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:43
+msgid "Hostel Allocation Closing Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:49
+msgid "Booking Session"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:57
+msgid "Allowed States"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:47
+msgid "Bed J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:48
+msgid "Bed K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:49
+msgid "Bed L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:62
+msgid "Block J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:75
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:104
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:425
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:432
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:443
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:632
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:659
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:673
+msgid "Next Matriculation Number Integer"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:700
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:706
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:724
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:729
+msgid "Payment disabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1054
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1112
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1124
+msgid "Process transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "Save comment and mark as processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+msgid "Transcript processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1223
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1295
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1298
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1301
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1383
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1391
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1393
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1395
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1451
+msgid "Enter valid course code"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1742
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1768
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:210
+msgid "Previous Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2161
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2491
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2495
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2504
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2793
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2882
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2949
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2955
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3131
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3138
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:405
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:415
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:572
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:612
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:631
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:702
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:711
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:712
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:719
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:720
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:802
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:856
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:871
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:932
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:934
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:939
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+msgid "Course Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+msgid "Current Study Mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+msgid "and"
+msgstr ""
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+msgid "Subjects/Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:255
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:285
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:345
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:382
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:449
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:482
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:343
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:590
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:596
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:639
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:735
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:741
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:781
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+msgid "Payment approved."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+msgid "Study Modes Group"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:218
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:126
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:145
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:187
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:197
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:470
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:521
+msgid "Matriculation number cannot be set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:543
+msgid "Matriculation number already set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:545
+msgid "No certificate assigned."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:552
+msgid "Matriculation number exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:720
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:820
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:847
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:185
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:194
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:203
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:359
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:607
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:640
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:658
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:667
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:683
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:839
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:902
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:105
+msgid "Reset to 'cleared'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:112
+msgid "School fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:118
+msgid "Pay PG school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:119
+msgid "PG school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:125
+msgid "Approve school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:126
+msgid "School fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:132
+msgid "Approve PG school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:133
+msgid "PG school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:140
+msgid "Reset to 'returning'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+msgid "Reset to school fee paid"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+msgid "Reset to 'school fee paid'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+msgid "Course validation bypassed"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:182
+msgid "Returned"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:188
+msgid "Reset to courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:189
+msgid "Reset to 'courses validated'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:197
+msgid "Request transript"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:198
+msgid "Transcript requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:29
+msgid "Record created"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:35
+msgid "Admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:42
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:56
+msgid "Reset to 'admitted'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+msgid "Reset to clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+msgid "Reset to 'clearance started'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:91
+msgid "First school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:98
+msgid "First school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/kofa/university/export.py:191
+msgid "Courses in Certificates"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:142
+msgid "Former course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:195
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:200
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:205
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:210
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:281
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "Postgraduate Level"
+msgstr ""
+
+#: waeup/kofa/userscontainer.py:79
+msgid "Users"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po	(revision 16045)
@@ -0,0 +1,58 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"PO-Revision-Date: 2012-04-13 10:18+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Germany <henrik@waeup.org>\n"
+"Language: en-US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: \n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr "--"
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr "--"
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr "--"
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr "--"
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr "--"
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr "yes"
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr "no"
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,7198 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Kofa\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2012-05-08 06:29+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Niger <isouaba@waeup.org>\n"
+"Language: fr-FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: French\n"
+"X-Poedit-Country: NIGER\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr "Annuller"
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr "Création du lot annullée"
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr "Réimporter"
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr "Réimportez les lots de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr "Réimportez annuller"
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr "Aucun fichier selectionné. Action est annulée."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr "Ce lot existe déjà: ${a}"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr "Réimportation terminée avec succès: ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr "Recherche et Gestion des codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr "Recherche "
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr "Déconnectez ACs"
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr "Activez ACs"
+
+#: waeup/kofa/accesscodes/browser.py:157
+#, fuzzy
+msgid "Cancel Search"
+msgstr "Annuller"
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr "Désactivé(e)"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr "${a} désactivé(e)"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr "${a}: Désactivation de Transition non autorisée."
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr "Disponible à nouveau"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr "${a} redisponible à nouveau."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr "${a}: Transition initiale non permise"
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr "Ajoutez le lot du code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr "Réimporter le lot du code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr "Recherche des codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr "Codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr "Archive"
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr "Archivage et suppression"
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr "Lots de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr "Aucun lot sélectionné."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr "Archivée ${a} (${b})"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr "Lot supprimé ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr "Créer le lot de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr "Créer le lot"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr "Lot créé (entrées ${a})"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr "Données écrites à ${a}"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+#, fuzzy
+msgid "Entries"
+msgstr "Requêtes"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+#, fuzzy
+msgid "Disabled"
+msgstr "Désactivé(e)"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr "Coût"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr "Date de création"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr "Les lots suivants sont disponibles:"
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr "Aucun lot pour l'instant."
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr "Préfixe"
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr "Aucun lots à importer n'est disponible"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr "avec PIN"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr "avec série"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr "avec fin de message"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr "Série"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr "AC"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr "pour les codes d'accès"
+
+#: waeup/kofa/accesscodes/export.py:38
+#, fuzzy
+msgid "AccessCodeBatches"
+msgstr "Lots de code d'accès"
+
+#: waeup/kofa/accesscodes/export.py:69
+#, fuzzy
+msgid "AccessCodes"
+msgstr "Codes d'accès"
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr "Numéro de série dans le lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr "Préfixe dans le lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr "Numéro du lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr "Partie aléatoire du code d'accès"
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr "Coût du code d'accès"
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr "État d’avancement"
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr " Titre complet du code d'accès"
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr "Acheteur"
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr "L'historique du code d'accès donné par les lignes"
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr "Date de création"
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr "Auteur du lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr "Préfixe du lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr "Numéro du lot (1-3 chiffres)"
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr "Nombre des codes d'accès"
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr "Nombre de codes d'accès non valides dans le lot"
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr "Nombre de codes d'accès utilisés dans le lot"
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr "Invalider PIN utilisés"
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr "Reactiver les PIN désactivés"
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr "Initialisé"
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr "Utilisé"
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr "Initialiser PIN"
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr "Utiliser PIN"
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr "Désactiver les PIN non utilisés"
+
+#: waeup/kofa/applicants/applicant.py:128
+#, fuzzy
+msgid "Applicant has not yet been admitted."
+msgstr "La demande n'a pas encore commencé."
+
+#: waeup/kofa/applicants/applicant.py:134
+#, fuzzy
+msgid "Registration Number exists."
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/applicants/applicant.py:137
+#, fuzzy
+msgid "No course admitted provided."
+msgstr "Aucun portrait chargé."
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+#, fuzzy
+msgid "Student ${a} created"
+msgstr "Dossier d’étudiant créé"
+
+#: waeup/kofa/applicants/batching.py:211
+#, fuzzy
+msgid "Application record imported"
+msgstr "Données de la demande créer."
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr "Le formulaire demandé est verrouillé (lecture seule)."
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+#, fuzzy
+msgid "Find applicants"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr "Aucune photo de passeport enregistrée."
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr "Case de confirmation de photo de passeport non sélectionnée."
+
+#: waeup/kofa/applicants/browser.py:103
+#, fuzzy
+msgid "Find applicant"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+#, fuzzy
+msgid "The form cannot be submitted. Wrong state!"
+msgstr "Ce formulaire ne peut pas être validé. État incorrect!"
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr "Le formulaire a été validé."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1120
+#, fuzzy
+msgid "Outside application period."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+#, fuzzy
+msgid "Send login credentials to email address"
+msgstr "Obtenir les autorisations d'ouverture"
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+#, fuzzy
+msgid "An error occurred."
+msgstr "Une erreur du serveur smtp produite."
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+#, fuzzy
+msgid "No application record found."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+#, fuzzy
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+"Le mot de passe a été déjà saisi. Vous utilisez le mauvais Code d’Accès."
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr "Chaîne de recherche vide"
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr "Vous avez été inscrit avec succès au"
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr "Votre inscription a été enregistrée avec succès."
+
+#: waeup/kofa/applicants/browser.py:122
+#, fuzzy
+msgid "No applicant found."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:1222
+#, fuzzy
+msgid "Data Exports"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+#, fuzzy
+msgid "Export started."
+msgstr "Importation échouée."
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr "Gerer la zone de demande"
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr "Sauvegarder"
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr "Supprimer sélectionné"
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr "Ajoutez le récipient du candidat"
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr "Supprimer les rôles locaux sélectionnés"
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr "Ajouter rôle local"
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr "Espace Candidat"
+
+#: waeup/kofa/applicants/browser.py:159
+#, fuzzy
+msgid "No container selected!"
+msgstr "Aucun ticket sélectionné."
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr "Ne pourrait être supprimer:"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr "Supprimer avec succès: ${a}"
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr "Formulaire a été Sauvegardé"
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+"Un récipient du demandeurs pour la meme demande et d'année d'entrée, existe "
+"déjà dans la base de données."
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr "Ajouté:"
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr "Demandeurs"
+
+#: waeup/kofa/applicants/browser.py:258
+#, fuzzy
+msgid "Applicant Data Exports"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+#, fuzzy
+msgid "Create students from selected"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr "Gérer les blocs des demandes"
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr "Aucun demandeur sélectionné !"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+#, fuzzy
+msgid "${a} students successfully created."
+msgstr "Cours ${a} créé avec succès."
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+#, fuzzy
+msgid "No student could be created."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr "Créer les données de la demande"
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr "Données de la demande créer."
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr "Demandeur "
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr "Prêt"
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr "Non prêt"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+#, fuzzy
+msgid "${a} <br /> Application Record ${b}"
+msgstr "${a} Données de la demande ${b}"
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr "Ce type de paiement a été déjà fait."
+
+#: waeup/kofa/applicants/browser.py:66
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr "Ticket de paiement créé."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr "${a}: Ticket de paiement en ligne ${b}"
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr "Données de paiement"
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+#, fuzzy
+msgid "Online Payment Slip"
+msgstr "Reçu du paiement en ligne"
+
+#: waeup/kofa/applicants/browser.py:742
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr "Section demande"
+
+#: waeup/kofa/applicants/browser.py:763
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr "Image trop grande pour téléchargement!"
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr "L’extension attendue du fichier est jpg"
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr "Dernière Validation"
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr "Supprimer les tickets sélectionnés"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+#, fuzzy
+msgid "${a} <br /> Application Form ${b}"
+msgstr "${a} formulaire de demande ${b}"
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr "Pas de transition"
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr "Aucun paiement sélectionné."
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+#, fuzzy
+msgid "Number of Submissions"
+msgstr "Nombre des codes d'accès"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr "État de la demande"
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+#, fuzzy
+msgid "Number of Applicants"
+msgstr "Demandeurs"
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr "Admis au cours:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+#, fuzzy
+msgid "Passport Photo:"
+msgstr "Photo d’Identité"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr "Taille de fichier Max.:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr "Transition de demande:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr "Je confirme que la photo sur ce formulaire est ma vrai photo."
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+#, fuzzy
+msgid "Payment Tickets"
+msgstr "Paiements"
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+"Un compte sera créé pour vous et un email avec vos coordonnées d'ouverture "
+"sera envoyé à l'adresse fournie."
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+#, fuzzy
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+"Un compte sera créé pour vous et un email avec vos coordonnées d'ouverture "
+"sera envoyé à l'adresse fournie."
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr "Numéro de la demande"
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr "Premier choix"
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr ""
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr "Année"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+#, fuzzy
+msgid "Introduction"
+msgstr "Instructions:"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr "Période de la demande"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+#, fuzzy
+msgid "with email address"
+msgstr "Adresse email"
+
+#: waeup/kofa/applicants/export.py:39
+#, fuzzy
+msgid "Basic Applicants Containers"
+msgstr "Espace Candidat"
+
+#: waeup/kofa/applicants/interfaces.py:144
+#, fuzzy
+msgid "Create Application Records"
+msgstr "Créer les données de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:145
+#, fuzzy
+msgid "Update Application Records"
+msgstr "Données de demande"
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+#, fuzzy
+msgid "Human readable description in HTML format"
+msgstr "Description lisible dans le format reST"
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr "Code"
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr "Titre"
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr "Demande Cible"
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr "Année d'entrée"
+
+#: waeup/kofa/applicants/interfaces.py:219
+#, fuzzy
+msgid "Application Mode"
+msgstr "Période de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr "Catégorie pour le regroupement des certificats"
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr "Date du début de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr "Date limite de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr "Interdiction d'ajout après la date-limite (date de clôture)"
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+#, fuzzy
+msgid "Application Fee"
+msgstr "Demande réinitialisée"
+
+#: waeup/kofa/applicants/interfaces.py:276
+#, fuzzy
+msgid "Hide container"
+msgstr "Gérer les blocs des demandes"
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr "Id du demandeur"
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr "Prénom"
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr "Deuxième prénom"
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr "Nom de famille"
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr "Date de naissance"
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr "Sexe"
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr "Adresse email"
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr "Téléphone"
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr "Premier choix "
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr "Deuxième choix "
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr "Notification"
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr "Id de l'étudiant"
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr "Admis au cours"
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr "Formulaire véroué"
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+#, fuzzy
+msgid "Special Application"
+msgstr "Demande"
+
+#: waeup/kofa/applicants/interfaces.py:520
+#, fuzzy
+msgid "Identification Number"
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+#, fuzzy
+msgid "Payment approved"
+msgstr "Date de Paiement"
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+#, fuzzy
+msgid "Successful payment"
+msgstr "Ajouté avec succès ${a}."
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr "Ce billet a déjà été payé"
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr "Données de demande"
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+#, fuzzy
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+"<font size=10>Aller à la page d'accueil et entrer vos nouvelles coordonnées: "
+"nom d'utilisateur= ${a}, mot de passe = ${b}.</font>"
+
+#: waeup/kofa/applicants/utils.py:41
+#, fuzzy
+msgid "Desired Study Courses"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/applicants/utils.py:42
+#, fuzzy
+msgid "Process Data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr "L'objet de configuration de la session n'est pas valide."
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+#, fuzzy
+msgid "Amount could not be determined."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr "Demande d' inscription"
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:137
+#, fuzzy
+msgid "Manage container"
+msgstr "Gérer les blocs des demandes"
+
+#: waeup/kofa/applicants/viewlets.py:147
+#, fuzzy
+msgid "Export applicants"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+#, fuzzy
+msgid "Create students"
+msgstr "Créer étudiant"
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr "Gérer les données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr "Éditer les données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr "Télécharger la demande en veille"
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+#, fuzzy
+msgid "Download payment slip"
+msgstr "Télécharger le reçu du paiement"
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+#, fuzzy
+msgid "Approve payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr "Demande"
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr "Mes Données"
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr "Dossier d’étudiant créé"
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr "Réinitialiser la demande pour commencer"
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr "Demande réinitialisée"
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr "Réinitialiser la demande a payée"
+
+#: waeup/kofa/applicants/workflow.py:144
+#, fuzzy
+msgid "Application reset to paid"
+msgstr "Demande réinitialisée"
+
+#: waeup/kofa/applicants/workflow.py:157
+#, fuzzy
+msgid "Reset application to admitted"
+msgstr "Réinitialiser la demande a payée"
+
+#: waeup/kofa/applicants/workflow.py:158
+#, fuzzy
+msgid "Application reset to admitted"
+msgstr "Demande réinitialisée"
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr "Démarrée"
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr "Payé"
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr "validé"
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr "admis"
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr "non admis(e)"
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr "créé"
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr "Initialisation de la demande"
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr "Demande initialisée"
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr "Démarrer la demande"
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr "Demande démarrée"
+
+#: waeup/kofa/applicants/workflow.py:66
+#, fuzzy
+msgid "Pay application fee"
+msgstr "Démarrer la demande"
+
+#: waeup/kofa/applicants/workflow.py:67
+#, fuzzy
+msgid "Payment made"
+msgstr "Date de Paiement"
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr "Valider la demande"
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr "Demande transmise"
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr "Admettre le candidat"
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr "Candidat admis"
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr "Refuser la demande"
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr "Demande refusée"
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr "Paramètres du portail"
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr "Universitaires"
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr "Administration"
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr "Configuration du portail"
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr "Les utilisateurs du portail"
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr "Centre des données"
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+#, fuzzy
+msgid "Reports"
+msgstr "Réimporter"
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+#, fuzzy
+msgid "Student Data Exports"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr "Accueil"
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr "Aucun rôle local sélectionné."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr "Une erreur du serveur smtp produite."
+
+#: waeup/kofa/browser/pages.py:1030
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1045
+#, fuzzy
+msgid "Only csv files are allowed."
+msgstr "Actuellement aucun de fichier journal n’est valable"
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr "Traitement fichier CSV"
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr "Retour à l’étape 1"
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr "Passer à l’étape 3"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr "Role local supprimé avec success: ${a}"
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+"Deux en-têtes: chaque nom de colonne doit apparaître qu’une seule fois."
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr "Remplacer le fichier importé!"
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr "Importation échouée."
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr "Mettre à jour mode seulement!"
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr "Mettre l’en-tête des champ"
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr "Retour à l’étape 2"
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr "Effectuer l’importation"
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr "Aucun element sélectionné."
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr "Editer les en-têtes ou remplacer le fichier importé"
+
+#: waeup/kofa/browser/pages.py:1422
+#, fuzzy
+msgid "Process next"
+msgstr "Processeur:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr "Traitement des ${a} lignes erronées."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr "${a} lignes traitées avec succès."
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr "Afficher les journaux "
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr "Afficher"
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+"Les fichiers de log ne peuvent être cherches seulement sur les systèmes "
+"d’exploitation basés sur l’Unix."
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1528
+#, fuzzy
+msgid "No search results found."
+msgstr "Pas d’enregistrement d’étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr "Editer les paramètres du centre de données"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr "Fichier déjà existant (ne pas copié): ${a}"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+#, fuzzy
+msgid "Given storage path cannot be used. ${a}"
+msgstr "Le Chemin de stockage donné ne peut pas être utilisé."
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr "Le nouveau chemin de stockage a été défini avec succès."
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1577
+#, fuzzy
+msgid "Create CSV file"
+msgstr "Transférer le fichier CSV"
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr "Section Universitaire"
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr "Ajouter faculté"
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr "Les facultés"
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr "Gestion des sections universitaires"
+
+#: waeup/kofa/browser/pages.py:1721
+#, fuzzy
+msgid "You are not allowed to remove entire faculties."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr "Le code de faculté choisi existe déjà."
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr "Départements"
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr "Ajouter département"
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr "Gérer la faculté"
+
+#: waeup/kofa/browser/pages.py:1807
+#, fuzzy
+msgid "You are not allowed to remove entire departments."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+#, fuzzy
+msgid "Find student(s)"
+msgstr "Ajouter étudiant"
+
+#: waeup/kofa/browser/pages.py:1847
+#, fuzzy
+msgid "Find students in "
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr "Le code choisi existe déjà dans cette faculté."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr "Département ${a} ajouté."
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr "Cours et Certificats"
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr "Supprimer les cours sélectionnés"
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr "Ajouter cours"
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr "Supprimer les certificats sélectionnés"
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr "Ajouter certificat"
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr "Gérer département"
+
+#: waeup/kofa/browser/pages.py:1996
+#, fuzzy
+msgid "You are not allowed to remove certificates."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr "Un cours avec le même code existe déjà: "
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr "Cours ${a} créé avec succès."
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr "Un certificat avec le même code existe déjà:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr "Certificat ${a} créé avec succès."
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr "Editer cours"
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr "Editer certificat"
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+#, fuzzy
+msgid "Add certificate course"
+msgstr "Ajouter certificat"
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+#, fuzzy
+msgid "Remove selected certificate courses"
+msgstr "Supprimer les certificats sélectionnés"
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr "Gérer certificact"
+
+#: waeup/kofa/browser/pages.py:2244
+#, fuzzy
+msgid "The chosen certificate course is already part of this certificate."
+msgstr "Le référant du cours choisi est déjà une partie de ce certificat."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+#, fuzzy
+msgid "certificate course ${a}_${b} added."
+msgstr "Référant du cours ${a}_${b} ajouté"
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+#, fuzzy
+msgid "Edit certificate course"
+msgstr "Editer certificat"
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr "Enregistrer et revenir"
+
+#: waeup/kofa/browser/pages.py:2325
+#, fuzzy
+msgid "Send me a new password"
+msgstr "Changer mot de passe"
+
+#: waeup/kofa/browser/pages.py:2373
+#, fuzzy
+msgid "No record found."
+msgstr "Pas d’enregistrement d’étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+#, fuzzy
+msgid "You have successfully requested a password for the"
+msgstr "Vous avez changé votre mot de passe avec succès pour le"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+"Un email contenant votre nom d’utilisateur et mot de passé a été envoyé à "
+"${a}."
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr "Connexion"
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr "Vous etes connecté"
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+#, fuzzy
+msgid "Your account has been temporarily deactivated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/browser/pages.py:324
+#, fuzzy
+msgid "Your account has been deactivated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/browser/pages.py:327
+#, fuzzy
+msgid "You entered invalid credentials."
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/browser/pages.py:342
+#, fuzzy
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr "Vous avez été déconnecté. Merci d’utiliser WAeUP SIRP!"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr "Envoyer un message maintenant"
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr "Votre message a été envoyé."
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr "Envoyer maintenant"
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr "Une erreur de serveur smtp s’est produite."
+
+#: waeup/kofa/browser/pages.py:469
+#, fuzzy
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr "<h1> Bienvenue à WAeUP.SIRP<h1>"
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr "Gérer"
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr "Supprimer"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr "Le compte d’utilisateur ${a} supprimé avec succès."
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr "Ajouter utilisateur"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr "Compte utilisateur ${a} ajouté avec succès."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr "Editer utilisateur ${a}"
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr "Les paramètres de l’utilisateur ont été sauvegardés."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr "Envoyer message à ${a}"
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr "Mes Préférences"
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr "Mes Roles"
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr "Chercher section universitaire"
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr "Voir la configuration du portail"
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr "Editer la configuration du portail"
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr "Mise à jour modules"
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr "Ajouter configuration de session"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr "Configuration ${a} Session"
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+"Les modules ont été mis à jour. Voir le fichier des journal pour plus de "
+"détails."
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr "Ajouter Configuration de Session"
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr "La session choisie existe déjà."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr "Editer configuration ${a} session universitaire"
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+#, fuzzy
+msgid "Successfully deleted: ${a}"
+msgstr "Supprimer avec succès: ${a}"
+
+#: waeup/kofa/browser/pages.py:932
+#, fuzzy
+msgid "Processed Files"
+msgstr "Traitement fichier CSV"
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr "Retour au centre des données"
+
+#: waeup/kofa/browser/pages.py:946
+#, fuzzy
+msgid "Upload portal data as CSV file"
+msgstr "Transférer le fichier CSV"
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr "Télécharger (vers le serveur)"
+
+#: waeup/kofa/browser/pages.py:995
+#, fuzzy
+msgid "Import Manager"
+msgstr "Importation échouée."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+#, fuzzy
+msgid "${a}: ${b} uploaded"
+msgstr "Fichier ${a} chargé."
+
+#: waeup/kofa/browser/pdf.py:156
+#, fuzzy
+msgid "Date:"
+msgstr "Date"
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr "Date"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+#, fuzzy
+msgid "Date: ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/kofa/browser/reports.py:112
+#, fuzzy
+msgid "Create report"
+msgstr "Auteur"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr "Code Cours:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr "Titre du cours:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr "Fourni par:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr "Niveau:"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+#, fuzzy
+msgid "Required course:"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr "Code:"
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+#, fuzzy
+msgid "Certificate Courses"
+msgstr "Certificats"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr "Semestre"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+#, fuzzy
+msgid "Cert. Course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+#, fuzzy
+msgid "Dep. Course"
+msgstr "Cours"
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr "resoudre_captcha"
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr "Paramètres du Site"
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr "Configurations de Sessions"
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+#, fuzzy
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+"Veuillez résoudre le captcha<br />pour empêcher l'usage abusif de ce "
+"service :"
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr "Date du téléchargement"
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr "Etape 1"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Nom du fichier"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr "Ensemble des données"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr "Taille"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+"En utilisant le traitement par lot vous pouvez créer en série, mettre à jour "
+"en série ou supprimer en série l’ensemble des données de la base de données "
+"en utilisant le fichier CSV."
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr "Sélectionner"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+"S’il vous plait sélectionner un fichier pour le traitement à partir de la "
+"liste suivante."
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr "Etape 2"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr "Mode:"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+"S’il vous plait sélectionner un fichier de traitement et un mode de "
+"traitement à partir des sélections ci-dessous."
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr "Champs d’en-tête OK"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#, fuzzy
+msgid "change to"
+msgstr "changer à:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr "Etape 3"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+"Eventuellement modifier l’en-tête des champs du fichier d’importation ci-"
+"dessous."
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr "Etape 4"
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr "Mode de traitement:"
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr "Traitement par lots terminé."
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr "Actuellement aucun de fichier journal n’est valable"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+#, fuzzy
+msgid "Storage path"
+msgstr "Chemin de stockage:"
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+"Le centre des données vous aide à gérer les données du portail. Vous pouvez "
+"télécharger ici le fichier CSV, qui sera disponible pour l’importation de la "
+"suite."
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Chemin de stockage:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Fichier:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr "Processeur:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+#, fuzzy
+msgid "Import Mode:"
+msgstr "Importation échouée."
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+#, fuzzy
+msgid "Processor"
+msgstr "Processeur:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+#, fuzzy
+msgid "Uploaded Files"
+msgstr "Transférer le fichier"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr "Rôle Local"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr "Rôles Locaux"
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+#, fuzzy
+msgid "There are no subobjects registered yet."
+msgstr "Il n’y a pas encore des sous objets enregistrés.\""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+#, fuzzy
+msgid "Password"
+msgstr "Mot de passe"
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+"Note: Les noms d’utilisateur, les Identifiants et les mots de passé sont des "
+"cas sensible."
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr "Problème de connexion1"
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr "Problème de connexion2"
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+#, fuzzy
+msgid "login_trouble4"
+msgstr "Problème de connexion1"
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr "Problème de connexion3"
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+#, fuzzy
+msgid "User Name or Id"
+msgstr "Nom d’Utilisateur ou Identifiant"
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr "Mes Rôles de Portail:"
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr "Mes Rôles locaux:"
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr "La page que vous tentez d’accéder n’est pas disponible."
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr "Vous essayez peut-être d’accéder à page non existante."
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr "S’il vous plait noter les points suivants:"
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr "Il se pourrait que vous ayez mal écrit l’URL."
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+#, fuzzy
+msgid "Report Number"
+msgstr "Numéro Reg."
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+#, fuzzy
+msgid "Description"
+msgstr "Description de Réponse"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+#, fuzzy
+msgid "Create new report"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr "Contact"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr "Déconnexion"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr "Ajouter"
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr "Envoyer un email"
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr "Editer les Paramètres"
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+#, fuzzy
+msgid "Export student data"
+msgstr "pour les étudiants"
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr "Rechercher la section universitaire"
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+#, fuzzy
+msgid "Find students"
+msgstr "Ajouter étudiant"
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr "Montrer les étudiants"
+
+#: waeup/kofa/browser/viewlets.py:518
+#, fuzzy
+msgid "Update scores"
+msgstr "Valider Cours"
+
+#: waeup/kofa/browser/viewlets.py:574
+#, fuzzy
+msgid "Show logs"
+msgstr "Afficher les journaux "
+
+#: waeup/kofa/browser/viewlets.py:583
+#, fuzzy
+msgid "Upload data"
+msgstr "Date du téléchargement"
+
+#: waeup/kofa/browser/viewlets.py:592
+#, fuzzy
+msgid "Process data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:610
+#, fuzzy
+msgid "View processed files"
+msgstr "Consulter le journal de traitement"
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr "Requêtes"
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr "Editer"
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr "Section Accommodation"
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr "Gérer la section d’accommodation"
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr "Ajouter dortoir"
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+#, fuzzy
+msgid "Clear all hostels"
+msgstr "Créer dortoir"
+
+#: waeup/kofa/hostels/browser.py:160
+#, fuzzy
+msgid "All hostels cleared."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr "Les paramètres ont été enregistrés."
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr "Créer dortoir"
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr "Ce dortoir existe déjà."
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr "Gérer dortoir"
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr "Mettre à jour tous les lits"
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr "Echanger la réservation des lits sélectionnés"
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr "Libérer les lits sélectionnés"
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+#, fuzzy
+msgid "Clear hostel"
+msgstr "Créer dortoir"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+"${a} lits vides supprimés, ${b} lits ajoutés, ${c} lits occupés modifiés "
+"(${d})"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr "Lits échangés avec succès: ${a}"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr "Lits libérés avec succès: ${a}"
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr "Pas de lit attributé sélectionné."
+
+#: waeup/kofa/hostels/browser.py:320
+#, fuzzy
+msgid "Hostel cleared."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr "Allouer étudiant"
+
+#: waeup/kofa/hostels/browser.py:339
+#, fuzzy
+msgid "No valid student id."
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr "Dortoirs"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr "Bloque ${a}, Chambre ${b}, Lit ${c}"
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr "Paramètres"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr "Id"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr "Nom"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+#, fuzzy
+msgid "Booked Beds"
+msgstr "Lit B"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr "Type"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr "Numéro "
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr "Propriétaire"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr "Données Dortoir"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr "[allouer étudiant]"
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+#, fuzzy
+msgid "Reserved Beds:"
+msgstr "Lits réservés"
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr "Lits"
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr "non réservé"
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr "Réservé"
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr "Plancher par Bloque"
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr "Chambre par Plancher"
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr "Lits réservés"
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr "Bloques des Etudiantes filles"
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr "Boques des Etudiants garçons"
+
+#: waeup/kofa/hostels/interfaces.py:141
+#, fuzzy
+msgid "Beds for Pre-Study Students"
+msgstr "Lits pour les nouveaux Etudiant"
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr "Lits pour les nouveaux Etudiant"
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr "Lits pour les anciens étudiant"
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr "Lits pour les étudiants de dernière année"
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr "Lits sans catégorie"
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr "Détenu spécial"
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr "Bloques des filles et des garçons se chévochent"
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr "Catégories de lit se chévochent."
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr "Id lit"
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr "Type de lit"
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr "Numéro du lit"
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr "Propriétaire (Etudiants)"
+
+#: waeup/kofa/hostels/interfaces.py:243
+#, fuzzy
+msgid "Enter valid student id."
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/hostels/interfaces.py:258
+#, fuzzy
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr "Votre session actuelle ne correspond pas la session d’hébergement."
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+#, fuzzy
+msgid "Hostel Allocation Start Date"
+msgstr "Date du début de la demande"
+
+#: waeup/kofa/hostels/interfaces.py:43
+#, fuzzy
+msgid "Hostel Allocation Closing Date"
+msgstr "Date limite de la demande"
+
+#: waeup/kofa/hostels/interfaces.py:49
+#, fuzzy
+msgid "Booking Session"
+msgstr "en session"
+
+#: waeup/kofa/hostels/interfaces.py:57
+#, fuzzy
+msgid "Allowed States"
+msgstr "Etat de Paiement"
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr "Id du dortoir"
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr "Trier Id"
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr "Nom du dortoir"
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr "Lit A"
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr "Lit B"
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr "Lit C"
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr "Lit D"
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr "Lit E"
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr "Lit F"
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr "Lit G"
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr "Lit H"
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr "Lit I"
+
+#: waeup/kofa/hostels/vocabularies.py:47
+#, fuzzy
+msgid "Bed J"
+msgstr "Lit A"
+
+#: waeup/kofa/hostels/vocabularies.py:48
+#, fuzzy
+msgid "Bed K"
+msgstr "Lit A"
+
+#: waeup/kofa/hostels/vocabularies.py:49
+#, fuzzy
+msgid "Bed L"
+msgstr "Lit A"
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr "Bloque B"
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr "Bloque C"
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr "Bloque D"
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr "Bloque E"
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr "Bloque F"
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr "Bloque G"
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr "Bloque H"
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr "Bloque I"
+
+#: waeup/kofa/hostels/vocabularies.py:62
+#, fuzzy
+msgid "Block J"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr "Bloque K"
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr "Bloque L"
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr "Bloque M"
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr "Bloque N"
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr "Bloque O"
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr "Bloque P"
+
+#: waeup/kofa/hostels/vocabularies.py:69
+#, fuzzy
+msgid "Block Q"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:70
+#, fuzzy
+msgid "Block R"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:71
+#, fuzzy
+msgid "Block S"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:72
+#, fuzzy
+msgid "Block T"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:73
+#, fuzzy
+msgid "Block U"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:74
+#, fuzzy
+msgid "Block V"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:75
+#, fuzzy
+msgid "Block W"
+msgstr "Bloque A"
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr "cours enregistrés"
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr "cours validés"
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr "Ancien (ne)"
+
+#: waeup/kofa/interfaces.py:103
+#, fuzzy
+msgid "graduated"
+msgstr "créé"
+
+#: waeup/kofa/interfaces.py:104
+#, fuzzy
+msgid "transcript requested"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+#, fuzzy
+msgid "Processor name"
+msgstr "Processeur:"
+
+#: waeup/kofa/interfaces.py:425
+#, fuzzy
+msgid "Email Address:"
+msgstr "Adresse email"
+
+#: waeup/kofa/interfaces.py:432
+#, fuzzy
+msgid "Email to:"
+msgstr "Email"
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:443
+#, fuzzy
+msgid "Full Name:"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+#, fuzzy
+msgid "Public Name"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+#, fuzzy
+msgid "Number of failed logins"
+msgstr "Nombre des codes d'accès"
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr "Id de l’utilisateur"
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Rôles Portail"
+
+#: waeup/kofa/interfaces.py:602
+#, fuzzy
+msgid "Name of University"
+msgstr "Nom du département"
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+#, fuzzy
+msgid "Name of Administrator"
+msgstr "Administration"
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:632
+#, fuzzy
+msgid "Email Address of Administrator"
+msgstr "Adresse email"
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+#, fuzzy
+msgid "Kofa Contact"
+msgstr "Contact"
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:659
+#, fuzzy
+msgid "Carry-over Course Registration"
+msgstr "Démarrer l’inscription de cours"
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+#, fuzzy
+msgid "Current Academic Session"
+msgstr "Section Universitaire"
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:673
+#, fuzzy
+msgid "Next Matriculation Number Integer"
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+#, fuzzy
+msgid "callbacks"
+msgstr "Demander un rappel"
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+#, fuzzy
+msgid "Academic Session"
+msgstr "Section Universitaire"
+
+#: waeup/kofa/interfaces.py:700
+msgid "Acceptance Fee"
+msgstr "Frais d’Acceptation"
+
+#: waeup/kofa/interfaces.py:706
+#, fuzzy
+msgid "Bed Booking Fee"
+msgstr "Date de Réservation"
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+#, fuzzy
+msgid "Transcript Fee"
+msgstr "Transfère"
+
+#: waeup/kofa/interfaces.py:724
+#, fuzzy
+msgid "Clearance enabled"
+msgstr "acquittement démarré"
+
+#: waeup/kofa/interfaces.py:729
+#, fuzzy
+msgid "Payment disabled"
+msgstr "Date de Paiement"
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr "acquittement démarré"
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr "acquitté"
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr "Frais de scolarité payés"
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:72
+#, fuzzy
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+"Le mot de passe a été déjà saisi. Vous utilisez le mauvais Code d’Accès."
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr "par"
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr "Code d’Activation"
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr "Réponse totale approuvée"
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr "Code Réponse"
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr "Description de Réponse"
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr "Non encore payé"
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr "Payé"
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr "Manqué"
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr "Catégorie de Paiement"
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr "Item de Paiement"
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr "Paiement pour la session"
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr "Etat de Paiement"
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr "Date de Création du Ticket "
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr "Date de Paiement"
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr "Montant Autorisé"
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:175
+#, fuzzy
+msgid "Unnamed Report"
+msgstr "Gérer certificact"
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr "Le niveau d’étude a été ajouté."
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr "Ce niveau existe."
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr "Aucun niveau d’étude sélectionné."
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+#, fuzzy
+msgid "Request transcript"
+msgstr "Demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:1054
+#, fuzzy
+msgid "Submit"
+msgstr "Dernière Validation"
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr "Mauvais état"
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr "Le code d’activation est invalid."
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr "Le code d’activation a été déjà utilisé."
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr "invalidé"
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr "Vous n’etes pas le propriétaire de ce code d’accès."
+
+#: waeup/kofa/students/browser.py:1112
+#, fuzzy
+msgid "Transcript processing has been started."
+msgstr "Le traitement d’acquittement a été démarré."
+
+#: waeup/kofa/students/browser.py:1124
+#, fuzzy
+msgid "Process transcript request"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/students/browser.py:1125
+#, fuzzy
+msgid "Save comment and mark as processed"
+msgstr "Sauvegarder et demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+#, fuzzy
+msgid "Transcript processed"
+msgstr "Transfère"
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+#, fuzzy
+msgid "You are not allowed to view the transcript."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+#, fuzzy
+msgid "${a}: Transcript Data"
+msgstr "${a}: Donnée de d’acquittement"
+
+#: waeup/kofa/students/browser.py:1223
+#, fuzzy
+msgid "You are not allowed to download the transcript."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+#, fuzzy
+msgid "Academic Transcript"
+msgstr "Universitaires"
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr "Crédits"
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr "Score"
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+#, fuzzy
+msgid "Transfer student"
+msgstr "Créer étudiant"
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr "Transfère"
+
+#: waeup/kofa/students/browser.py:1292
+#, fuzzy
+msgid "Current level does not match certificate levels."
+msgstr "Votre session actuelle ne correspond pas la session d’hébergement."
+
+#: waeup/kofa/students/browser.py:1295
+#, fuzzy
+msgid "Former study course record incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/browser.py:1298
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1301
+#, fuzzy
+msgid "Successfully transferred."
+msgstr "Ajouté avec succès ${a}."
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+#, fuzzy
+msgid "Revert now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr "${a}: Niveau d’Etude ${b}"
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr "Données Niveau"
+
+#: waeup/kofa/students/browser.py:1383
+#, fuzzy
+msgid "Course Registration Slip"
+msgstr "Imprimé des cours d' inscription"
+
+#: waeup/kofa/students/browser.py:1391
+#, fuzzy
+msgid "1st Semester Courses"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/students/browser.py:1393
+#, fuzzy
+msgid "2nd Semester Courses"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/students/browser.py:1395
+#, fuzzy
+msgid "Level Courses"
+msgstr "Code du Niveau"
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr "Dépt."
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr "Faculté"
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr "Créd."
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr "Ajouter ticket de cours"
+
+#: waeup/kofa/students/browser.py:1451
+#, fuzzy
+msgid "Enter valid course code"
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+#, fuzzy
+msgid "Course not found."
+msgstr "Aucun étudiant trouvé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr "Gérer niveau étude ${a}"
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr "Aucun ticket sélectionné."
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr "Ce niveau ne correspond pas au niveau actuel."
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr "L’étudiant est dans le faux état."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+#, fuzzy
+msgid "Total credits exceed ${a}."
+msgstr "Crédits Total:"
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr "Demande de liste de cours a été annulée."
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr "Demande de liste de cours a été rejetée:"
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr "Ce ticket existe."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr "${a}: Ticket de cours ${b}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr "Ajouté avec succès ${a}."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr "Gérer ticket de cours ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr "${a}: Paiements"
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr "Créer ticket"
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr "Votre session actuelle ne correspond pas la session d’hébergement."
+
+#: waeup/kofa/students/browser.py:1742
+#, fuzzy
+msgid "You have not yet booked accommodation."
+msgstr "Réserver chambre"
+
+#: waeup/kofa/students/browser.py:1768
+#, fuzzy
+msgid "Add previous session online payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+#, fuzzy
+msgid "Add balance"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr "Etudiants"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr "${a}: Hébergement"
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr "Vous n’etes pas autorisé à supprimer les tickets de lit."
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr "Pas de ticket de lit Sélectionné."
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr "Ajouter ticket de lit"
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr "Créer ticket de lit"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr "Vous etes dans l’état d’enregistrement erroné."
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+"Vous avez déjà reserve un espace lit dans la session hebergement actuelle."
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr "Dans votre catégorie il n’y a plus de lit libre ${a}."
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:210
+#, fuzzy
+msgid "Previous Study Course"
+msgstr "Cours d' étude"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr "${a}, Bloque ${b}, Chambre ${c}, Lit ${d} (${e})"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr "Ticket de lit créé et lit reservé: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr "Ticket de lit pour la session ${a}"
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr "Données d’allocation de lit"
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr "Paiements"
+
+#: waeup/kofa/students/browser.py:2161
+#, fuzzy
+msgid "Bed Allocation Slip"
+msgstr "Allocation Lit:"
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr "les étudiants en état de lit reservé ne peuvent pas être ré-alloquer."
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr "Etudiant ne peut pas être déplacé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr "Etudiant déplacé: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr "${a}: Histoire"
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr "Editer données de base"
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr "Changer mot de passe"
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr "Mot de passé changé."
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr "Hébergement"
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr "Charger portrait"
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr "Démarrer l’acquittement"
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr "Aucun portrait chargé."
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr "Pas tous les champs demandés qui sont remplis."
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr "Le traitement d’acquittement a été démarré."
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr "Editer les données d’acquittement"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr "Ticket de lit ${a}"
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr "Le formulaire d’acquittement a été sauvegardé"
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr "Sauvegarder et demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr "Ce formulaire ne peut pas être validé. État incorrect!"
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr "Acquittement a été demandé."
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr "Demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr "Entrer le code d’accès CLR pour démarrer l’acquittement"
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr "Demander l’acquittement maintenant"
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr "Ce n’est pas votre code d’accès CLR."
+
+#: waeup/kofa/students/browser.py:2491
+#, fuzzy
+msgid "Start session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2495
+#, fuzzy
+msgid "Start now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:2504
+#, fuzzy
+msgid "You are not entitled to start session."
+msgstr "Vous etes dans l’état d’enregistrement erroné."
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+#, fuzzy
+msgid "Session started."
+msgstr "Demande démarrée"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr "Ajouter le niveau actuel ${a}"
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+#, fuzzy
+msgid "Your data are incomplete"
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr "Créer la liste de cours maintenant"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+#, fuzzy
+msgid "Edit course list of ${a}"
+msgstr "Editer utilisateur ${a}"
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr "La liste de cours a été enregistrée."
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr "Registre de liste de cours"
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr "Mettre un mot de passe pour la première connexion"
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr "Mettre"
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr "Plus d’un étudiant trouvé."
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr "Le code d’accès est invalide."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr "Le mot de passe a été déjà saisi. Votre Id Etudiant est ${a}"
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+"Le mot de passe a été déjà saisi. Vous utilisez le mauvais Code d’Accès."
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr "Le code d’accès a été déjà utilisé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr "Le mot de passe a été saisi. Votre Id d’étudiant est ${a}"
+
+#: waeup/kofa/students/browser.py:2793
+#, fuzzy
+msgid "Request password for first-time login"
+msgstr "Mettre un mot de passe pour la première connexion"
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+#, fuzzy
+msgid "No student record found."
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2882
+#, fuzzy
+msgid "Your password request was successful."
+msgstr "Votre inscription a été enregistrée avec succès."
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr "Session Actuelle"
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr "Niveau Actuel"
+
+#: waeup/kofa/students/browser.py:2949
+#, fuzzy
+msgid "in any session"
+msgstr "en session"
+
+#: waeup/kofa/students/browser.py:2955
+#, fuzzy
+msgid "at any level"
+msgstr "Niveau de démarrage"
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr "Niveau"
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr "Gérer la section étudiant"
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+#, fuzzy
+msgid "Current academic session not set."
+msgstr "Rechercher la section universitaire"
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+#, fuzzy
+msgid "Configure student data export"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/students/browser.py:3131
+#, fuzzy
+msgid "All Sessions"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:3138
+#, fuzzy
+msgid "All Levels"
+msgstr "Niveau"
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr "Ajouter étudiant"
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr "Données étudiant créé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:405
+#, fuzzy
+msgid "Login now"
+msgstr "Connexion"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+#, fuzzy
+msgid "Login as ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/kofa/students/browser.py:415
+#, fuzzy
+msgid "You successfully logged in as student."
+msgstr "Vous avez changé votre mot de passe avec succès pour le"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr "${a}: Données de Base"
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr "Gestion données de base"
+
+#: waeup/kofa/students/browser.py:572
+#, fuzzy
+msgid "Trigger registration transition"
+msgstr "Transition d’Inscription:"
+
+#: waeup/kofa/students/browser.py:612
+#, fuzzy
+msgid "Student account has been activated."
+msgstr "L’étudiant a été acquitté."
+
+#: waeup/kofa/students/browser.py:631
+#, fuzzy
+msgid "Student account has been deactivated."
+msgstr "L’étudiant a été acquitté."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr "${a}: Donnée de d’acquittement"
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr "Donnée d’acquittement "
+
+#: waeup/kofa/students/browser.py:702
+#, fuzzy
+msgid "Clearance Slip of"
+msgstr "Imprimé d'acquittement de"
+
+#: waeup/kofa/students/browser.py:711
+#, fuzzy
+msgid "Student Signature"
+msgstr "Section Etudiants"
+
+#: waeup/kofa/students/browser.py:712
+#, fuzzy
+msgid "Clearance Officer Signature"
+msgstr "Aquittement"
+
+#: waeup/kofa/students/browser.py:719
+#, fuzzy
+msgid "Date, Student Signature"
+msgstr "Section Etudiants"
+
+#: waeup/kofa/students/browser.py:720
+#, fuzzy
+msgid "Date, Clearance Officer Signature"
+msgstr "Aquittement"
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr "Gérer les données d’acquittement"
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr "L’étudiant a été acquitté."
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr "Etudiant est dans un faux état."
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr "Acquittement Rejeté"
+
+#: waeup/kofa/students/browser.py:802
+#, fuzzy
+msgid "Save comment and reject clearance now"
+msgstr "Sauvegarder et demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr "L’acquittement a été annulé."
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr "Demande d’acquittement rejetée."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr "${a}: Donnée à caractère personnel"
+
+#: waeup/kofa/students/browser.py:856
+#, fuzzy
+msgid "Manage personal data"
+msgstr "Gérer les données à caractère personnel"
+
+#: waeup/kofa/students/browser.py:871
+#, fuzzy
+msgid "Edit personal data"
+msgstr "Gérer les données à caractère personnel"
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr "${a}: Cours d' étude"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+#, fuzzy
+msgid "${a}: Previous Study Course"
+msgstr "${a}: Cours d' étude"
+
+#: waeup/kofa/students/browser.py:932
+#, fuzzy
+msgid "First Study Course, "
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:934
+#, fuzzy
+msgid "Second Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:939
+#, fuzzy
+msgid "First Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr "Gérer Cours d' étude"
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr "Supprimer les niveaux sélectionés"
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr "Ajouter niveau d’étude"
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr "Coordonnées du lit"
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr "Catégorie de Lit"
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+#, fuzzy
+msgid "Retype Password:"
+msgstr "Resaisir le mot de passe"
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr "Scans"
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr "Tout sélectionné"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr "avec nom"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr "avec numéro d’inscription"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr "avec numéro matricule"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr "en département"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr "en mode d’étude"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr "Résultats de la Recherche"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr "Numéro Reg."
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr "Numéro Matricule."
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr "Etat"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr "avec id"
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr "Statut"
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+#, fuzzy
+msgid "Course Level"
+msgstr "Niveau Actuel"
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+#, fuzzy
+msgid "Matric No."
+msgstr "Numéro Matricule."
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr "Nom complet"
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+#, fuzzy
+msgid "Current Study Mode"
+msgstr "Mode d’Etude:"
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+#, fuzzy
+msgid "Export Number"
+msgstr "Numéro "
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr "Auteur"
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+#, fuzzy
+msgid "and"
+msgstr "Mand."
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+#, fuzzy
+msgid "login_as_intro_1"
+msgstr "Problème de connexion1"
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+#, fuzzy
+msgid "login_as_intro_2"
+msgstr "Problème de connexion1"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr "Catégorie"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr "Item"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr "Paiement Id"
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr "Date de Création"
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+#, fuzzy
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+"Un compte sera créé pour vous et un email avec vos coordonnées d'ouverture "
+"sera envoyé à l'adresse fournie."
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+"Veuillez résoudre le captcha<br />pour empêcher l'usage abusif de ce "
+"service :"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr "L'email ne peut pas être envoyé. Réessayez plus tard."
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+#, fuzzy
+msgid "Thanks for using Kofa!"
+msgstr "Merci d'avoir utiliser SIRP !"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+#, fuzzy
+msgid "Your login credentials are:"
+msgstr "Obtenir les autorisations d'ouverture"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+#, fuzzy
+msgid "User Name:"
+msgstr "Nom d’Utilisateur ou Identifiant"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr "Mot de passe"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr "Un email avec votre nom d'utilisateur et mot de passe a été envoyé à"
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr "Veuillez vérifier votre compte email pour procéder."
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr "Code d’Accès:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr "Instructions:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+"<li> Acquérir un mot de passe (PWD) du code d’accès. </li> <li> Entrer votre "
+"numéro d’inscription (BAC) et le code accès. </li> <li> Après avoir appuyé "
+"sur le bouton, votre carte d’étudiant sera affiché et votre mot de passe "
+"défini. La partie à 10 chiffres du code d’accès utilisé est tout simplement "
+"votre nouveau mot de passe. </li> <li> Aller à la <strong><a href=\"login"
+"\">page de connexion générale</a></strong> et entrer vos nouvelles "
+"informations d’identification. </li>"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr "Numéro d’inscription"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr "Code du Niveau"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr "Titre du Niveau"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr "Données cours d’étude"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr "en session"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+#, fuzzy
+msgid "Previous Study Courses:"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr "Les Niveaux d’Etude (Listes de Cours)"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr "Il n’ya pas encore des niveaux enregistrés."
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr "Texte qui explique la procedure d’inscription au cours."
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr "CO"
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+#, fuzzy
+msgid "Enter valid course code."
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+#, fuzzy
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr "Tickets de Cours (Crédits Total: ${DYNAMIC_CONTENT})"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr "Fact."
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr "Donnée Niveau Etude"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr "Auto"
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr "Il n’y a encore aucun ticket ajouté."
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+#, fuzzy
+msgid "Subjects/Courses"
+msgstr "Rejeter Cours"
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+#, fuzzy
+msgid "Process request"
+msgstr "Processeur:"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+#, fuzzy
+msgid "transcript_request"
+msgstr "Transfère"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+#, fuzzy
+msgid "Dispatch Address:"
+msgstr "Adresse email"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr "Code Activation:"
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+#, fuzzy
+msgid "Send new request"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/students/export.py:255
+#, fuzzy
+msgid "Student Study Courses"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/export.py:285
+#, fuzzy
+msgid "Student Study Levels"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr "Tickets de Cours"
+
+#: waeup/kofa/students/export.py:345
+#, fuzzy
+msgid "Student Payments"
+msgstr "Etudiants admis"
+
+#: waeup/kofa/students/export.py:382
+#, fuzzy
+msgid "Payment Data for Bursary"
+msgstr "Données de paiement"
+
+#: waeup/kofa/students/export.py:413
+#, fuzzy
+msgid "Bed Tickets"
+msgstr "Ticket de lit ${a}"
+
+#: waeup/kofa/students/export.py:449
+#, fuzzy
+msgid "Student Payments Overview"
+msgstr "Etudiants admis"
+
+#: waeup/kofa/students/export.py:482
+#, fuzzy
+msgid "Student Study Levels Overview"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+#, fuzzy
+msgid "Reasons for Deactivation"
+msgstr "Description de Réponse"
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr "PWD Code Activation"
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr "Email"
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr "Formulaire d’acquittement vérouillé"
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr "CLR Code Activation"
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr "Nationalité"
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:343
+#, fuzzy
+msgid "Updated"
+msgstr "Mettre à jour tous les lits"
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr "Adresse Permanente"
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+#, fuzzy
+msgid "Registr. or Matric. Number"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr "Certificat"
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr "Mode d’Entrée"
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr "Session d’Entrée"
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr "Verdict Actuel"
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr "Verdict Précédent"
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+#, fuzzy
+msgid "Validated by"
+msgstr "invalidé"
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr "Verdict"
+
+#: waeup/kofa/students/interfaces.py:590
+#, fuzzy
+msgid "Validation Date"
+msgstr "Date de Création"
+
+#: waeup/kofa/students/interfaces.py:596
+#, fuzzy
+msgid "Total Credits"
+msgstr "Crédits Total:"
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+#, fuzzy
+msgid "Faculty Code"
+msgstr "Faculté"
+
+#: waeup/kofa/students/interfaces.py:639
+#, fuzzy
+msgid "Department Code"
+msgstr "Département"
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr "Trimestre/Semestre"
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr "Moyenne"
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr "Création Automatique"
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr "Cours"
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+#, fuzzy
+msgid "Level Session"
+msgstr "Session"
+
+#: waeup/kofa/students/interfaces.py:735
+#, fuzzy
+msgid "Allocated Bed"
+msgstr "Allouer étudiant"
+
+#: waeup/kofa/students/interfaces.py:741
+#, fuzzy
+msgid "Requested Bed Type"
+msgstr "Type de lit"
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr "Date de Réservation"
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr "Code Activation de Réservation"
+
+#: waeup/kofa/students/interfaces.py:781
+#, fuzzy
+msgid "Current Session Payment"
+msgstr "Session Actuelle"
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+#, fuzzy
+msgid "Payment Level"
+msgstr "Item de Paiement"
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+#, fuzzy
+msgid "Payment approved."
+msgstr "Date de Paiement"
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr "Département"
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+#, fuzzy
+msgid "Study Modes Group"
+msgstr "Mode d’Etude:"
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+#, fuzzy
+msgid "Level Report"
+msgstr "Code du Niveau"
+
+#: waeup/kofa/students/reports/level_report.py:218
+#, fuzzy
+msgid "Create level report"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+#, fuzzy
+msgid "Session Results Presentation"
+msgstr "Configurations de Sessions"
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+#, fuzzy
+msgid "Student Statistics"
+msgstr "Section Etudiants"
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+#, fuzzy
+msgid "Create student statistics report"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/students/utils.py:126
+#, fuzzy
+msgid "Name:"
+msgstr "Nom"
+
+#: waeup/kofa/students/utils.py:145
+#, fuzzy
+msgid "Study Course:"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr "Département:"
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr "Faculté"
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr "Mode d’Etude:"
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+#, fuzzy
+msgid "Entry Session:"
+msgstr "Session d’Entrée"
+
+#: waeup/kofa/students/utils.py:187
+#, fuzzy
+msgid "Current Level:"
+msgstr "Niveau Actuel"
+
+#: waeup/kofa/students/utils.py:197
+#, fuzzy
+msgid "Date of Birth:"
+msgstr "Date de naissance"
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr "Documents Scannés"
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+#, fuzzy
+msgid "Study course data are incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/utils.py:470
+#, fuzzy
+msgid "no bed allocated"
+msgstr "Télécharger imprimé du lit alloué"
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+#, fuzzy
+msgid "Amount must be greater than 0."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/students/utils.py:521
+#, fuzzy
+msgid "Matriculation number cannot be set."
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/students/utils.py:543
+#, fuzzy
+msgid "Matriculation number already set."
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/students/utils.py:545
+#, fuzzy
+msgid "No certificate assigned."
+msgstr "Ajouter certificat"
+
+#: waeup/kofa/students/utils.py:552
+#, fuzzy
+msgid "Matriculation number exists."
+msgstr "Numéro d’Immatriculation"
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+#, fuzzy
+msgid "Your Kofa student record was created on ${a}."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr "Données de Base"
+
+#: waeup/kofa/students/utils.py:720
+#, fuzzy
+msgid "Workflow History"
+msgstr "État d’avancement"
+
+#: waeup/kofa/students/utils.py:820
+#, fuzzy
+msgid "Course of Study:"
+msgstr "Admis au cours:"
+
+#: waeup/kofa/students/utils.py:847
+#, fuzzy
+msgid "Entry Mode:"
+msgstr "Mode d’Entrée"
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr "Histoire"
+
+#: waeup/kofa/students/viewlets.py:185
+#, fuzzy
+msgid "Trigger transition"
+msgstr "Pas de transition"
+
+#: waeup/kofa/students/viewlets.py:194
+#, fuzzy
+msgid "Login as student"
+msgstr "pour les étudiants"
+
+#: waeup/kofa/students/viewlets.py:203
+#, fuzzy
+msgid "Download admission letter"
+msgstr "Télécharger la demande en veille"
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr "Etudiant Acquitté"
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr "Télécharger imprimé d' acquittement"
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:359
+#, fuzzy
+msgid "Transcript"
+msgstr "Transfère"
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr "Valider Cours"
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr "Rejeter Cours"
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr "Télécharger imprimé des cours inscrit"
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr "Réserver chambre"
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr "Télécharger imprimé du lit alloué"
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr "Relocaliser étudiant"
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr "Changer portrait"
+
+#: waeup/kofa/students/viewlets.py:607
+#, fuzzy
+msgid "Start new session"
+msgstr "Session"
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr "Ajouter liste de cours"
+
+#: waeup/kofa/students/viewlets.py:640
+#, fuzzy
+msgid "Edit course list"
+msgstr "Ajouter liste de cours"
+
+#: waeup/kofa/students/viewlets.py:658
+#, fuzzy
+msgid "Add current session payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:667
+#, fuzzy
+msgid "Add previous session payment ticket"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:683
+#, fuzzy
+msgid "Add balance payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr "Données d’hébergement"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr "${a} supprimé."
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr "Le fichier envoyé est trop grand."
+
+#: waeup/kofa/students/viewlets.py:839
+#, fuzzy
+msgid "Could not determine file type."
+msgstr "Ne pourrait être supprimer:"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr "${a} l’extension de fichier attendue."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+#, fuzzy
+msgid "Only the following extensions are allowed: ${a}"
+msgstr "Seulement les extensions suivantes sont autorisées: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr "Fichier ${a} chargé."
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr "Fichier"
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/kofa/students/viewlets.py:902
+#, fuzzy
+msgid "Upload selected file"
+msgstr "Envoyer un nouveau fichier"
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr "Imprimé de demande"
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr "Aucun fichier local sélectioné"
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr "Photo d’Identité"
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr "Photo d’Identité (jpg seulement)"
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr "Extrait d’Acte de Naissance"
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr "Birth Certificate Scan"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr "${title} 2ème dérogation"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr "${title} 3ème dérogation"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr "${title} 1ere dérogation"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr "${title} 1er rédoublement"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr "${title} 2ème rédoublement"
+
+#: waeup/kofa/students/vocabularies.py:141
+#, fuzzy
+msgid "male"
+msgstr "Féminin"
+
+#: waeup/kofa/students/vocabularies.py:143
+#, fuzzy
+msgid "female"
+msgstr "Féminin"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+#, fuzzy
+msgid "Error: wrong level id ${value}"
+msgstr "Erreur: niveau id ${value} en dépassement"
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr "Erreur: niveau id ${value} en dépassement"
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr "Réinitialisation pour acquitter"
+
+#: waeup/kofa/students/workflow.py:105
+#, fuzzy
+msgid "Reset to 'cleared'"
+msgstr "Réinitialisation pour acquitter"
+
+#: waeup/kofa/students/workflow.py:112
+#, fuzzy
+msgid "School fee payment made"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/students/workflow.py:118
+#, fuzzy
+msgid "Pay PG school fee"
+msgstr "Payer les frais de scolarité"
+
+#: waeup/kofa/students/workflow.py:119
+#, fuzzy
+msgid "PG school fee payment made"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/students/workflow.py:125
+#, fuzzy
+msgid "Approve school fee payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/workflow.py:126
+#, fuzzy
+msgid "School fee payment approved"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/students/workflow.py:132
+#, fuzzy
+msgid "Approve PG school fee payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/workflow.py:133
+#, fuzzy
+msgid "PG school fee payment approved"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr "Réinitialiser pour revenir"
+
+#: waeup/kofa/students/workflow.py:140
+#, fuzzy
+msgid "Reset to 'returning'"
+msgstr "Réinitialiser pour revenir"
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr "Cours enregistrés"
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+#, fuzzy
+msgid "Reset to school fee paid"
+msgstr "Frais de scolarité payés"
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+#, fuzzy
+msgid "Reset to 'school fee paid'"
+msgstr "Frais de scolarité payés"
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr "Cours validés"
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+#, fuzzy
+msgid "Course validation bypassed"
+msgstr "Cours validés"
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr "Retour"
+
+#: waeup/kofa/students/workflow.py:182
+msgid "Returned"
+msgstr "Retourné"
+
+#: waeup/kofa/students/workflow.py:188
+#, fuzzy
+msgid "Reset to courses validated"
+msgstr "Réinitialiser à valider"
+
+#: waeup/kofa/students/workflow.py:189
+#, fuzzy
+msgid "Reset to 'courses validated'"
+msgstr "Réinitialiser à valider"
+
+#: waeup/kofa/students/workflow.py:197
+#, fuzzy
+msgid "Request transript"
+msgstr "Demander l’acquittement"
+
+#: waeup/kofa/students/workflow.py:198
+#, fuzzy
+msgid "Transcript requested"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr "Créer étudiant"
+
+#: waeup/kofa/students/workflow.py:29
+#, fuzzy
+msgid "Record created"
+msgstr "Dossier d’étudiant créé"
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr "Admettre étudiant"
+
+#: waeup/kofa/students/workflow.py:35
+#, fuzzy
+msgid "Admitted"
+msgstr "admis"
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr "Réinitialisé etudiant"
+
+#: waeup/kofa/students/workflow.py:42
+#, fuzzy
+msgid "Reset to initial state"
+msgstr "Réinitialiser à valider"
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr "acquittement démarré"
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr "Réinitialiser à admettre"
+
+#: waeup/kofa/students/workflow.py:56
+#, fuzzy
+msgid "Reset to 'admitted'"
+msgstr "Réinitialiser à admettre"
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr "Acquittement demandé"
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+#, fuzzy
+msgid "Reset to clearance started"
+msgstr "Réinitialiser pour acquittement"
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+#, fuzzy
+msgid "Reset to 'clearance started'"
+msgstr "Réinitialiser pour acquittement"
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr "Acquitté"
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr "Payer les frais de scolarité"
+
+#: waeup/kofa/students/workflow.py:91
+#, fuzzy
+msgid "First school fee payment made"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/students/workflow.py:98
+#, fuzzy
+msgid "First school fee payment approved"
+msgstr "Frais de scolarités payés"
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr "Cours"
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr "Certificats"
+
+#: waeup/kofa/university/export.py:191
+#, fuzzy
+msgid "Courses in Certificates"
+msgstr "Cours et Certificats"
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr "Titre du cours"
+
+#: waeup/kofa/university/interfaces.py:142
+#, fuzzy
+msgid "Former course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr "Mode d’Etude"
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr "Niveau de démarrage"
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr "Niveau final"
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr "Catégorie de demande"
+
+#: waeup/kofa/university/interfaces.py:195
+#, fuzzy
+msgid "Initial School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:200
+#, fuzzy
+msgid "Returning School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:205
+#, fuzzy
+msgid "Foreigner Initial School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:210
+#, fuzzy
+msgid "Foreigner Returning School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:281
+#, fuzzy
+msgid "Registration required"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr "Nom de la Faculté"
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr "Préfixe du nom"
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr "Nom du département"
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr "Pré-études"
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr "100 (Année 1)"
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr "200 (Année 2)"
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr "300 (Année 3)"
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr "400 (Année 4)"
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr "500 (Année 5)"
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr "600 (Année 6)"
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr "700 (Année 7)"
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr "800 (Année 8)"
+
+#: waeup/kofa/university/vocabularies.py:39
+#, fuzzy
+msgid "900 (Year 9)"
+msgstr "100 (Année 1)"
+
+#: waeup/kofa/university/vocabularies.py:40
+#, fuzzy
+msgid "Postgraduate Level"
+msgstr "Dortoir pour le 3ème Cycle"
+
+#: waeup/kofa/userscontainer.py:79
+#, fuzzy
+msgid "Users"
+msgstr "Id de l’utilisateur"
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+#, fuzzy
+msgid "Algeria"
+msgstr "Série"
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+#, fuzzy
+msgid "Liberia"
+msgstr "Série"
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+#, fuzzy
+msgid "Nigeria"
+msgstr "Série"
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+#, fuzzy
+msgid "Portugal"
+msgstr "Les utilisateurs du portail"
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+#, fuzzy
+msgid "Suriname"
+msgstr "Nom du fichier"
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+#, fuzzy
+msgid "United States"
+msgstr "Etat de Paiement"
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+#, fuzzy
+msgid "Viet Nam"
+msgstr "Prénom"
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+#, fuzzy
+msgid "France"
+msgstr "Aquittement"
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+"Nom complet: ${a}\n"
+"Utilisateur Id: ${b}\n"
+"Type d’Itulisateur: ${c}\n"
+"Portail: ${d}\n"
+"\n"
+"${e}\n"
+
+#: waeup/kofa/utils/utils.py:249
+#, fuzzy
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+"Cher(e) ${a},\n"
+"\n"
+"${b}\n"
+"L’inscription de l’Etudiant et Information du portail de\n"
+"${c}.\n"
+"\n"
+"Votre nom d’utilisateur: ${d}\n"
+"Votre mot de passe: ${e}\n"
+"Page de connexion: ${f}\n"
+"\n"
+"S’il vous plaît se souvenir de votre nom d’utilisateur et garder le\n"
+"votre mot de passe confidentiel!\n"
+"\n"
+"S’il vous plaît noter également que ce mot de passe est Personnel.\n"
+"\n"
+"Cordialement\n"
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+#, fuzzy
+msgid "Invalid datetime data"
+msgstr "invalidé"
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+#, fuzzy
+msgid "remove-selected-items"
+msgstr "Supprimer les tickets sélectionnés"
+
+#, fuzzy
+#~ msgid "Application Fee Payment Tickets"
+#~ msgstr "Ticket de paiement des frais d’acceptation"
+
+#~ msgid "Back"
+#~ msgstr "Revenir"
+
+#~ msgid "Error: ${a}"
+#~ msgstr "Erreur: ${a}"
+
+#, fuzzy
+#~ msgid "Reload page"
+#~ msgstr "Date du téléchargement"
+
+#~ msgid "Overwrite contents in new location?"
+#~ msgstr "Ecraser le contenu de nouvel emplacement?"
+
+#~ msgid "Copy existing data to new storage?"
+#~ msgstr "Copier les données existantes vers un nouvel stockage?"
+
+#, fuzzy
+#~ msgid "Back to bottom"
+#~ msgstr "Retour au début"
+
+#~ msgid "Back to top"
+#~ msgstr "Retour au début"
+
+#~ msgid "Student Id:"
+#~ msgstr "Id de l’Etudiant"
+
+#~ msgid "State:"
+#~ msgstr "Etat:"
+
+#~ msgid "Student Section"
+#~ msgstr "Section Etudiants"
+
+#, fuzzy
+#~ msgid "Current Session:"
+#~ msgstr "Session Actuelle"
+
+#, fuzzy
+#~ msgid "Level Session:"
+#~ msgstr "Session"
+
+#, fuzzy
+#~ msgid "Set department filter."
+#~ msgstr "en mode d’étude"
+
+#, fuzzy
+#~ msgid "Select level to report."
+#~ msgstr "Rechercher la section universitaire"
+
+#, fuzzy
+#~ msgid "Set study modes filter."
+#~ msgstr "en mode d’étude"
+
+#, fuzzy
+#~ msgid "Select academic session to report."
+#~ msgstr "Rechercher la section universitaire"
+
+#~ msgid "Delete attachment"
+#~ msgstr "Supprimer lien"
+
+#, fuzzy
+#~ msgid "Search applicants"
+#~ msgstr "Démarrer la demande"
+
+#~ msgid "Sem."
+#~ msgstr "Sem."
+
+#~ msgid "for students"
+#~ msgstr "pour les étudiants"
+
+#~ msgid "Enter unique course code which will become part of the URL."
+#~ msgstr "Saisir un code unique de cours qui fera partie de l’URL."
+
+#, fuzzy
+#~ msgid "Unnamed Certificate"
+#~ msgstr "Gérer certificact"
+
+#~ msgid "Enter unique certificate code which will become part of the URL."
+#~ msgstr "Saisir un code unique de certificat qui fera partie de l’URL."
+
+#~ msgid "Unnamed"
+#~ msgstr "Anonyme"
+
+#~ msgid "This code will become part of the URL."
+#~ msgstr "Ce code fera partie de l’URL."
+
+#~ msgid "View processing log"
+#~ msgstr "Consulter le journal de traitement"
+
+#~ msgid "Back to data center"
+#~ msgstr "Revenir au centre des données"
+
+#, fuzzy
+#~ msgid "This export was already discarded."
+#~ msgstr "Ce billet a déjà été payé"
+
+#, fuzzy
+#~ msgid ""
+#~ "Please pick the type of objects you want to export from the selection "
+#~ "below."
+#~ msgstr ""
+#~ "S’il vous plait sélectionner un fichier de traitement et un mode de "
+#~ "traitement à partir des sélections ci-dessous."
+
+#~ msgid "Form Status:"
+#~ msgstr "Statut du Formulaire:"
+
+#~ msgid "Regular Hostel"
+#~ msgstr "Dortoir Ordinaire"
+
+#~ msgid "Blocked Hostel"
+#~ msgstr "Dortoir Bloqué"
+
+#~ msgid "Postgraduate Hostel"
+#~ msgstr "Dortoir pour le 3ème Cycle"
+
+#~ msgid "Payment Access Code"
+#~ msgstr "Code d’Accès Paiement"
+
+#~ msgid "School Fee"
+#~ msgstr "Frais Scolaires"
+
+#, fuzzy
+#~ msgid "Bed Allocation Fee"
+#~ msgstr "Allocation Lit:"
+
+#, fuzzy
+#~ msgid "Hostel Maintenance Fee"
+#~ msgstr "Entretien Dortoir"
+
+#, fuzzy
+#~ msgid "Transfer Fee"
+#~ msgstr "Transfère"
+
+#~ msgid "Course List"
+#~ msgstr "Liste de Cours"
+
+#~ msgid "Semester:"
+#~ msgstr "Semestre:"
+
+#~ msgid "Credits:"
+#~ msgstr "Crédits:"
+
+#~ msgid "Passmark:"
+#~ msgstr "Moyenne"
+
+#, fuzzy
+#~ msgid ""
+#~ "This type of payment has already been made. Would you like to pay for a "
+#~ "previous session?"
+#~ msgstr "Ce type de paiement a été déjà fait."
+
+#, fuzzy
+#~ msgid "${a} Upload"
+#~ msgstr "Télécharger (vers le serveur)"
+
+#~ msgid "Entries/(invalidated)"
+#~ msgstr "Entrées/(non validées)"
+
+#~ msgid "Is mandatory course (not elective):"
+#~ msgstr "Est cours obligatoir (non urgent):"
+
+#~ msgid "Mandatory"
+#~ msgstr "Obligatoire"
+
+#~ msgid "Is mandatory course (not elective)"
+#~ msgstr "Est cours obligatoire (pas prioritaire)"
+
+#~ msgid "Get login credentials"
+#~ msgstr "Obtenir les autorisations d'ouverture"
+
+#~ msgid "Screening Venue"
+#~ msgstr "Salle d'examen "
+
+#~ msgid "Screening Score"
+#~ msgstr "Note du test "
+
+#~ msgid "Upload file"
+#~ msgstr "Transférer le fichier"
+
+#~ msgid "Batch processing"
+#~ msgstr "Traitement par lots"
+
+#~ msgid "Clearance"
+#~ msgstr "Aquittement"
+
+#~ msgid "Bed Allocation"
+#~ msgstr "Allocation Lit"
+
+#~ msgid "Gown"
+#~ msgstr "Uniforme"
+
+#~ msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+#~ msgstr "<font size=10>${a} Page ${b} de ${c}</font>"
+
+#~ msgid "Acceptance Letter"
+#~ msgstr "Lettre d’acceptation"
+
+#~ msgid "Acceptance Letter Scan"
+#~ msgstr "Lettre d’Acceptation Scannée"
+
+#, fuzzy
+#~ msgid "Pay postgraduate school fee"
+#~ msgstr "Payer les frais de scolarité"
+
+#, fuzzy
+#~ msgid "Approve postgraduate payment"
+#~ msgstr "Ajouter paiement en ligne"
+
+#~ msgid "Reset to paid"
+#~ msgstr "Réinitialiser pour payer"
+
+#, fuzzy
+#~ msgid "Reset to 'paid'"
+#~ msgstr "Réinitialiser pour payer"
+
+#~ msgid "Student admitted"
+#~ msgstr "Etudiants admis"
+
+#~ msgid "School fee paid"
+#~ msgstr "Frais de scolarités payés"
+
+#~ msgid "Add course referrer"
+#~ msgstr "Ajouter référant du cours"
+
+#~ msgid "Remove selected course referrers"
+#~ msgstr "Supprimer les référant du cours sélectionnés"
+
+#~ msgid "Edit course referrer"
+#~ msgstr "Editer référant cours"
+
+#~ msgid "Course Referrers"
+#~ msgstr "Réferrants Cours"
+
+#~ msgid "Referrer"
+#~ msgstr "Réferrant"
+
+#~ msgid "Add and remove course tickets of study level ${a}"
+#~ msgstr "Ajouter et supprimer le ticket de cours du niveau d’étude ${a}"
+
+#~ msgid "Add and remove courses"
+#~ msgstr "Ajouter et supprimer les cours"
+
+#~ msgid "Course Referrer"
+#~ msgstr "Cours référent"
+
+#, fuzzy
+#~ msgid "Error: %s"
+#~ msgstr "Erreur: ${a}"
+
+#~ msgid "Application has not yet started."
+#~ msgstr "La demande n'a pas encore commencé."
+
+#~ msgid "Application has ended."
+#~ msgstr "Demande Cloturée."
+
+#~ msgid "There no subobjects registered yet."
+#~ msgstr "Il n’y a pas encore des sous objets enregistrés.\""
+
+#, fuzzy
+#~ msgid "Accommodation Booking Session"
+#~ msgstr "Section Accommodation"
+
+#, fuzzy
+#~ msgid "Portal roles"
+#~ msgstr "Rôles Portail"
+
+#~ msgid "Start course registration"
+#~ msgstr "Démarrer l’inscription de cours"
+
+#~ msgid "Start course registration now"
+#~ msgstr "Démarrer l’inscription de cours maintenant"
+
+#, fuzzy
+#~ msgid "You are in wrong state."
+#~ msgstr "Etudiant est dans un faux état."
+
+#~ msgid "Course registration has been started."
+#~ msgstr "L’inscription de cours a été démarré."
+
+#~ msgid "File name has no extension."
+#~ msgstr "Le nom du fichier n’a pas d’extension."
+
+#~ msgid "File name contains more than one dot."
+#~ msgstr "Le nom de fichier contient plus d’un point."
+
+#~ msgid "Valid callback received."
+#~ msgstr "Retour de confirmation reçu."
+
+#~ msgid "Fee paid"
+#~ msgstr "Frais payés"
+
+#~ msgid "Valid callback received. ${a}"
+#~ msgstr "Rappel valide reçu. ${a}"
+
+#~ msgid "Ticket not yet paid."
+#~ msgstr "Ticket non encore payé."
+
+#~ msgid "Register for ${a} Application"
+#~ msgstr "Inscription à ${a} demande "
+
+#~ msgid "Photograph"
+#~ msgstr "Photographie"
+
+#~ msgid "Entry Level"
+#~ msgstr "Niveau d'entrée"
+
+#~ msgid "Applicants Container Type"
+#~ msgstr "Type de récipient du demandeurs"
+
+#~ msgid "JAMB Registration Number"
+#~ msgstr "Numéro de matricule  "
+
+#~ msgid "State/LGA"
+#~ msgstr "Région/ville"
+
+#~ msgid "<font size=10>Change your password when you have  logged in.</font>"
+#~ msgstr ""
+#~ "<font size=10>Changer votre mot de passe une fois la session ouverte.</"
+#~ "font>"
+
+#~ msgid "Pay acceptance fee"
+#~ msgstr "Payer les frais d’acceptation"
+
+#~ msgid ""
+#~ "Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+#~ "Group, 2008-2012"
+#~ msgstr ""
+#~ "Information sur les Etudiants et l’Inscription au Portail <br /> "
+#~ "Copyright &copy; WAeUP Group, 2008-2012"
+
+#~ msgid "Show batch logs"
+#~ msgstr "Voir les journaux par lots"
+
+#~ msgid "Surcharge 1"
+#~ msgstr "Supplément 1"
+
+#~ msgid "Surcharge 2"
+#~ msgstr "Supplément 2"
+
+#~ msgid "Surcharge 3"
+#~ msgstr "Supplément 3"
+
+#~ msgid "Response Payment Reference"
+#~ msgstr "Référence de Paiement Réponse"
+
+#~ msgid "Response Card Number"
+#~ msgstr "Numéro de Carte Réponse"
+
+#~ msgid "Change my password"
+#~ msgstr "Changer mon mot de passe"
+
+#~ msgid "Get new login credentials"
+#~ msgstr "Obtentenez des nouvelles informations de connexion"
+
+#~ msgid "Male"
+#~ msgstr "Masculin"
+
+#~ msgid "Student record reset to 'returning'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à 'retourner"
+
+#~ msgid "Student record reset to 'paid'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à (payé)"
+
+#~ msgid "Student record reset to 'validated'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à 'validé'"
+
+#~ msgid "Student record reset"
+#~ msgstr "Dossier d’étudiant réinitialisé"
+
+#~ msgid "Student record reset to 'admitted'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à 'admis'"
+
+#~ msgid "Student record reset to 'clearance'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à 'acquitté'"
+
+#~ msgid "Student record reset to 'cleared'"
+#~ msgstr "Réinitilisation dossier de l’étudiant à 'acquitté'"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,6397 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2012-03-09 07:17+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Niger <isouaba@waeup.org>\n"
+"Language: ha-NG-Latn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: Hausa\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Cancel Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:38
+msgid "AccessCodeBatches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:69
+msgid "AccessCodes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:134
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:137
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:103
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1120
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:122
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1222
+msgid "Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+msgid "Export started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:159
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:66
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:742
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:763
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Payment Tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr ""
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr ""
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:145
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:219
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:520
+msgid "Identification Number"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:137
+msgid "Manage container"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:147
+msgid "Export applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+msgid "Approve payment"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:144
+msgid "Application reset to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:157
+msgid "Reset application to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:158
+msgid "Application reset to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:66
+msgid "Pay application fee"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:67
+msgid "Payment made"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr ""
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1030
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1045
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1422
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1528
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+msgid "Given storage path cannot be used. ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1577
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1721
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1807
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1847
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1996
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2244
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2325
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2373
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:327
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:342
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:469
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:932
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:946
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:995
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+msgid "Required course:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr ""
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+msgid "Password"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+msgid "User Name or Id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:518
+msgid "Update scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:583
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:592
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:610
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:160
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:320
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:339
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:141
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:243
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:258
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+msgid "Hostel Allocation Start Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:43
+msgid "Hostel Allocation Closing Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:49
+msgid "Booking Session"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:57
+msgid "Allowed States"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:47
+msgid "Bed J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:48
+msgid "Bed K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:49
+msgid "Bed L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:62
+msgid "Block J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:75
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:104
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:425
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:432
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:443
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:632
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:659
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:673
+msgid "Next Matriculation Number Integer"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:700
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:706
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:724
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:729
+msgid "Payment disabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1054
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1112
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1124
+msgid "Process transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "Save comment and mark as processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+msgid "Transcript processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1223
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1295
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1298
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1301
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1383
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1391
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1393
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1395
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1451
+msgid "Enter valid course code"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1742
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1768
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:210
+msgid "Previous Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2161
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2491
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2495
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2504
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2793
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2882
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2949
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2955
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3131
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3138
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:405
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:415
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:572
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:612
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:631
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:702
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:711
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:712
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:719
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:720
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:802
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:856
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:871
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:932
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:934
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:939
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+msgid "Course Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+msgid "Current Study Mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+msgid "and"
+msgstr ""
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+msgid "Subjects/Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:255
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:285
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:345
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:382
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:449
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:482
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:343
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:590
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:596
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:639
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:735
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:741
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:781
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+msgid "Payment approved."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+msgid "Study Modes Group"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:218
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:126
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:145
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:187
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:197
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:470
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:521
+msgid "Matriculation number cannot be set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:543
+msgid "Matriculation number already set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:545
+msgid "No certificate assigned."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:552
+msgid "Matriculation number exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:720
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:820
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:847
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:185
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:194
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:203
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:359
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:607
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:640
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:658
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:667
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:683
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:839
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:902
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:105
+msgid "Reset to 'cleared'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:112
+msgid "School fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:118
+msgid "Pay PG school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:119
+msgid "PG school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:125
+msgid "Approve school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:126
+msgid "School fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:132
+msgid "Approve PG school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:133
+msgid "PG school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:140
+msgid "Reset to 'returning'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+msgid "Reset to school fee paid"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+msgid "Reset to 'school fee paid'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+msgid "Course validation bypassed"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:182
+msgid "Returned"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:188
+msgid "Reset to courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:189
+msgid "Reset to 'courses validated'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:197
+msgid "Request transript"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:198
+msgid "Transcript requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:29
+msgid "Record created"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:35
+msgid "Admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:42
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:56
+msgid "Reset to 'admitted'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+msgid "Reset to clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+msgid "Reset to 'clearance started'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:91
+msgid "First school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:98
+msgid "First school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/kofa/university/export.py:191
+msgid "Courses in Certificates"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:142
+msgid "Former course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:195
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:200
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:205
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:210
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:281
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "Postgraduate Level"
+msgstr ""
+
+#: waeup/kofa/userscontainer.py:79
+msgid "Users"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,6397 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.KOFA\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2012-03-09 07:17+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Nigeria <isouaba@waeup.org>\n"
+"Language: ig-NG-Latn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: Igbo\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Cancel Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:38
+msgid "AccessCodeBatches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:69
+msgid "AccessCodes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:134
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:137
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:103
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1120
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:122
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1222
+msgid "Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+msgid "Export started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:159
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:66
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:742
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:763
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Payment Tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr ""
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr ""
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:145
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:219
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:520
+msgid "Identification Number"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:137
+msgid "Manage container"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:147
+msgid "Export applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+msgid "Approve payment"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:144
+msgid "Application reset to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:157
+msgid "Reset application to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:158
+msgid "Application reset to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:66
+msgid "Pay application fee"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:67
+msgid "Payment made"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr ""
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1030
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1045
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1422
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1528
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+msgid "Given storage path cannot be used. ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1577
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1721
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1807
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1847
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1996
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2244
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2325
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2373
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:327
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:342
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:469
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:932
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:946
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:995
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+msgid "Required course:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr ""
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+msgid "Password"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+msgid "User Name or Id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:518
+msgid "Update scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:583
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:592
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:610
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:160
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:320
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:339
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:141
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:243
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:258
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+msgid "Hostel Allocation Start Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:43
+msgid "Hostel Allocation Closing Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:49
+msgid "Booking Session"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:57
+msgid "Allowed States"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:47
+msgid "Bed J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:48
+msgid "Bed K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:49
+msgid "Bed L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:62
+msgid "Block J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:75
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:104
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:425
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:432
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:443
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:632
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:659
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:673
+msgid "Next Matriculation Number Integer"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:700
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:706
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:724
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:729
+msgid "Payment disabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1054
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1112
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1124
+msgid "Process transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "Save comment and mark as processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+msgid "Transcript processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1223
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1295
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1298
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1301
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1383
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1391
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1393
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1395
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1451
+msgid "Enter valid course code"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1742
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1768
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:210
+msgid "Previous Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2161
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2491
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2495
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2504
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2793
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2882
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2949
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2955
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3131
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3138
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:405
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:415
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:572
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:612
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:631
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:702
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:711
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:712
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:719
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:720
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:802
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:856
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:871
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:932
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:934
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:939
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+msgid "Course Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+msgid "Current Study Mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+msgid "and"
+msgstr ""
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+msgid "Subjects/Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:255
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:285
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:345
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:382
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:449
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:482
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:343
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:590
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:596
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:639
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:735
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:741
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:781
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+msgid "Payment approved."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+msgid "Study Modes Group"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:218
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:126
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:145
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:187
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:197
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:470
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:521
+msgid "Matriculation number cannot be set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:543
+msgid "Matriculation number already set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:545
+msgid "No certificate assigned."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:552
+msgid "Matriculation number exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:720
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:820
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:847
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:185
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:194
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:203
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:359
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:607
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:640
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:658
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:667
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:683
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:839
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:902
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:105
+msgid "Reset to 'cleared'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:112
+msgid "School fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:118
+msgid "Pay PG school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:119
+msgid "PG school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:125
+msgid "Approve school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:126
+msgid "School fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:132
+msgid "Approve PG school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:133
+msgid "PG school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:140
+msgid "Reset to 'returning'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+msgid "Reset to school fee paid"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+msgid "Reset to 'school fee paid'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+msgid "Course validation bypassed"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:182
+msgid "Returned"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:188
+msgid "Reset to courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:189
+msgid "Reset to 'courses validated'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:197
+msgid "Request transript"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:198
+msgid "Transcript requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:29
+msgid "Record created"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:35
+msgid "Admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:42
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:56
+msgid "Reset to 'admitted'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+msgid "Reset to clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+msgid "Reset to 'clearance started'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:91
+msgid "First school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:98
+msgid "First school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/kofa/university/export.py:191
+msgid "Courses in Certificates"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:142
+msgid "Former course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:195
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:200
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:205
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:210
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:281
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "Postgraduate Level"
+msgstr ""
+
+#: waeup/kofa/userscontainer.py:79
+msgid "Users"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/waeup.kofa.pot
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/waeup.kofa.pot	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/waeup.kofa.pot	(revision 16045)
@@ -0,0 +1,8204 @@
+##############################################################################
+#
+# Copyright (c) 2003-2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: Meaningless\n"
+"POT-Creation-Date: Wed Nov 20 14:20:03 2019\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope-dev@zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: waeup/kofa/accesscodes/browser.py:101
+#: waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:253
+#: waeup/kofa/applicants/browser.py:360
+#: waeup/kofa/applicants/browser.py:361
+#: waeup/kofa/applicants/browser.py:470
+#: waeup/kofa/applicants/browser.py:540
+#: waeup/kofa/applicants/browser.py:573
+#: waeup/kofa/browser/pages.py:969
+#: waeup/kofa/browser/pages.py:1256
+#: waeup/kofa/browser/pages.py:1281
+#: waeup/kofa/browser/pages.py:1570
+#: waeup/kofa/browser/pages.py:1674
+#: waeup/kofa/browser/pages.py:2093
+#: waeup/kofa/browser/pages.py:2115
+#: waeup/kofa/browser/pages.py:2150
+#: waeup/kofa/browser/pages.py:2179
+#: waeup/kofa/browser/pages.py:2180
+#: waeup/kofa/browser/pages.py:2204
+#: waeup/kofa/browser/pages.py:2300
+#: waeup/kofa/browser/pages.py:2344
+#: waeup/kofa/browser/pages.py:2345
+#: waeup/kofa/browser/pages.py:2347
+#: waeup/kofa/browser/pages.py:2402
+#: waeup/kofa/browser/pages.py:2452
+#: waeup/kofa/browser/pages.py:2494
+#: waeup/kofa/browser/pages.py:2527
+#: waeup/kofa/browser/pages.py:2543
+#: waeup/kofa/browser/pages.py:2591
+#: waeup/kofa/browser/pages.py:2593
+#: waeup/kofa/browser/pages.py:2615
+#: waeup/kofa/browser/pages.py:2657
+#: waeup/kofa/browser/pages.py:2728
+#: waeup/kofa/documents/browser.py:107
+#: waeup/kofa/documents/browser.py:153
+#: waeup/kofa/documents/browser.py:257
+#: waeup/kofa/documents/browser.py:275
+#: waeup/kofa/documents/browser.py:305
+#: waeup/kofa/documents/browser.py:334
+#: waeup/kofa/students/browser.py:994
+#: waeup/kofa/students/browser.py:995
+#: waeup/kofa/students/browser.py:1642
+#: waeup/kofa/students/browser.py:1644
+#: waeup/kofa/students/browser.py:1871
+#: waeup/kofa/students/browser.py:2043
+#: waeup/kofa/students/browser.py:2082
+#: waeup/kofa/students/browser.py:2117
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:137
+#. Default: ""
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:140
+#. Default: ""
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+#: waeup/kofa/browser/pages.py:1140
+#: waeup/kofa/browser/pages.py:1881
+#: waeup/kofa/browser/viewlets.py:203
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Cancel Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184
+#: waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:186
+#. Default: ""
+msgid "${a} disabled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:190
+#. Default: ""
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:196
+#. Default: ""
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:200
+#. Default: ""
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:266
+#: waeup/kofa/accesscodes/export.py:66
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:46
+#: waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+#: waeup/kofa/accesscodes/export.py:36
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:61
+#. Default: ""
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:69
+#. Default: ""
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:92
+#. Default: ""
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:94
+#. Default: ""
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:37
+#: waeup/kofa/applicants/workflow.py:42
+msgid "initialized"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:160
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:166
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:169
+msgid "No course admitted provided."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:188
+msgid "IOError: Application Slip could not be created."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:190
+msgid "Reportlab LayoutError: %s"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:192
+msgid "Unknown error."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:212
+#. Default: ""
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:101
+msgid "Applicant Processor"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:217
+msgid "Application record imported"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:266
+#: waeup/kofa/students/batching.py:235
+#. Default: ""
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:361
+msgid "ApplicantOnlinePayment Processor"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:51
+msgid "ApplicantsContainer Processor"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1013
+#. Default: ""
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1025
+#: waeup/kofa/documents/browser.py:369
+#: waeup/kofa/students/browser.py:615
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1070
+#: waeup/kofa/students/browser.py:1964
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:112
+#: waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:113
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1168
+#: waeup/kofa/students/browser.py:113
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1186
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1261
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1267
+msgid "Some invitation emails could not be sent:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:127
+#: waeup/kofa/browser/pages.py:2263
+#: waeup/kofa/students/browser.py:280
+#: waeup/kofa/students/browser.py:323
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1270
+#. Default: ""
+msgid "Form has been successfully submitted and ${a} invitation emails were sent."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:132
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1320
+#. Default: ""
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1325
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1330
+msgid "The portal is in maintenance mode and registration temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1354
+#: waeup/kofa/browser/pages.py:2802
+#: waeup/kofa/students/browser.py:3185
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1388
+#: waeup/kofa/students/browser.py:3202
+#: waeup/kofa/students/browser.py:3280
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1395
+#: waeup/kofa/applicants/browser.py:1408
+#: waeup/kofa/applicants/browser.py:1490
+#: waeup/kofa/applicants/browser.py:1493
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1399
+#: waeup/kofa/students/browser.py:3211
+msgid "Your password has already been set and used. Please proceed to the login page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:140
+#: waeup/kofa/applicants/viewlets.py:99
+msgid "Manage applicants section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1419
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:143
+#: waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:360
+#: waeup/kofa/applicants/browser.py:395
+#: waeup/kofa/applicants/browser.py:949
+#: waeup/kofa/applicants/browser.py:1028
+#: waeup/kofa/applicants/browser.py:1149
+#: waeup/kofa/applicants/browser.py:1152
+#: waeup/kofa/applicants/browser.py:1157
+#: waeup/kofa/applicants/browser.py:1160
+#: waeup/kofa/applicants/browser.py:1205
+#: waeup/kofa/browser/pages.py:940
+#: waeup/kofa/browser/pages.py:1169
+#: waeup/kofa/browser/pages.py:1183
+#: waeup/kofa/browser/pages.py:1276
+#: waeup/kofa/browser/pages.py:1923
+#: waeup/kofa/browser/pages.py:2179
+#: waeup/kofa/browser/pages.py:2200
+#: waeup/kofa/browser/pages.py:2344
+#: waeup/kofa/browser/pages.py:2372
+#: waeup/kofa/browser/pages.py:2527
+#: waeup/kofa/browser/pages.py:2539
+#: waeup/kofa/browser/pages.py:2591
+#: waeup/kofa/browser/pages.py:2601
+#: waeup/kofa/browser/pages.py:2701
+#: waeup/kofa/documents/browser.py:257
+#: waeup/kofa/documents/browser.py:271
+#: waeup/kofa/documents/browser.py:295
+#: waeup/kofa/documents/browser.py:324
+#: waeup/kofa/hostels/browser.py:163
+#: waeup/kofa/hostels/browser.py:199
+#: waeup/kofa/hostels/browser.py:321
+#: waeup/kofa/hostels/browser.py:333
+#: waeup/kofa/hostels/browser.py:447
+#: waeup/kofa/students/browser.py:565
+#: waeup/kofa/students/browser.py:618
+#: waeup/kofa/students/browser.py:781
+#: waeup/kofa/students/browser.py:887
+#: waeup/kofa/students/browser.py:994
+#: waeup/kofa/students/browser.py:1015
+#: waeup/kofa/students/browser.py:1642
+#: waeup/kofa/students/browser.py:1681
+#: waeup/kofa/students/browser.py:2247
+#: waeup/kofa/students/browser.py:2289
+#: waeup/kofa/students/browser.py:2589
+#: waeup/kofa/students/browser.py:2604
+#: waeup/kofa/students/browser.py:2736
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1436
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144
+#: waeup/kofa/applicants/browser.py:164
+#: waeup/kofa/applicants/browser.py:361
+#: waeup/kofa/applicants/browser.py:410
+#: waeup/kofa/browser/pages.py:1172
+#: waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1298
+#: waeup/kofa/browser/pages.py:2093
+#: waeup/kofa/browser/pages.py:2101
+#: waeup/kofa/browser/pages.py:2180
+#: waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/hostels/browser.py:166
+#: waeup/kofa/hostels/browser.py:171
+#: waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:2257
+#: waeup/kofa/students/browser.py:2301
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144
+#: waeup/kofa/applicants/browser.py:192
+#: waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:225
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145
+#: waeup/kofa/applicants/browser.py:201
+#: waeup/kofa/applicants/browser.py:363
+#: waeup/kofa/applicants/browser.py:479
+#: waeup/kofa/browser/pages.py:2181
+#: waeup/kofa/browser/pages.py:2218
+#: waeup/kofa/browser/pages.py:2348
+#: waeup/kofa/browser/pages.py:2411
+#: waeup/kofa/browser/pages.py:2528
+#: waeup/kofa/browser/pages.py:2552
+#: waeup/kofa/browser/pages.py:2594
+#: waeup/kofa/browser/pages.py:2624
+#: waeup/kofa/documents/browser.py:258
+#: waeup/kofa/documents/browser.py:284
+#: waeup/kofa/documents/browser.py:314
+#: waeup/kofa/documents/browser.py:343
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1451
+#: waeup/kofa/applicants/browser.py:1657
+#: waeup/kofa/students/browser.py:1103
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1455
+#. Default: ""
+msgid "Admission status of ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1457
+msgid "Check your admission status"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146
+#: waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:364
+#: waeup/kofa/applicants/browser.py:475
+#: waeup/kofa/browser/pages.py:2182
+#: waeup/kofa/browser/pages.py:2214
+#: waeup/kofa/browser/pages.py:2349
+#: waeup/kofa/browser/pages.py:2407
+#: waeup/kofa/browser/pages.py:2529
+#: waeup/kofa/browser/pages.py:2548
+#: waeup/kofa/browser/pages.py:2595
+#: waeup/kofa/browser/pages.py:2620
+#: waeup/kofa/documents/browser.py:259
+#: waeup/kofa/documents/browser.py:280
+#: waeup/kofa/documents/browser.py:310
+#: waeup/kofa/documents/browser.py:339
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147
+#: waeup/kofa/applicants/export.py:38
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1475
+msgid "Required input missing."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1508
+msgid "Application record invalid."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1539
+msgid "Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1552
+#: waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/students/browser.py:3855
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1567
+#: waeup/kofa/browser/pages.py:286
+msgid "Currently, exporters cannot be started due to high system load. Please try again later."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1594
+msgid "Exports started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1614
+msgid "Referee Report"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1627
+msgid "Add referee report"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1634
+msgid "The portal is in maintenance mode. Referee report forms are temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1645
+#: waeup/kofa/mandates/browser.py:42
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1658
+msgid "Are you really sure? Reports can neither be modified or added after submission."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1668
+msgid "Referee report has been saved. Thank you!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:170
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:181
+#: waeup/kofa/applicants/browser.py:427
+#: waeup/kofa/applicants/browser.py:1083
+msgid "Could not delete:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:184
+#: waeup/kofa/applicants/browser.py:430
+#: waeup/kofa/applicants/browser.py:1086
+#: waeup/kofa/browser/pages.py:164
+#: waeup/kofa/students/browser.py:351
+#: waeup/kofa/students/browser.py:1085
+#: waeup/kofa/students/browser.py:1702
+#: waeup/kofa/students/browser.py:1977
+#: waeup/kofa/students/browser.py:2322
+#: waeup/kofa/students/browser.py:2990
+#. Default: ""
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:211
+#: waeup/kofa/applicants/browser.py:405
+#: waeup/kofa/applicants/browser.py:1056
+#: waeup/kofa/applicants/browser.py:1211
+#: waeup/kofa/browser/pages.py:206
+#: waeup/kofa/browser/pages.py:2718
+#: waeup/kofa/hostels/browser.py:50
+#: waeup/kofa/students/browser.py:105
+#: waeup/kofa/students/browser.py:587
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:237
+msgid "An applicants container for the same application type and entrance year exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:247
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:261
+#: waeup/kofa/applicants/export.py:84
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:273
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:362
+#: waeup/kofa/applicants/browser.py:435
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:371
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:416
+#: waeup/kofa/applicants/browser.py:447
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:439
+msgid "You don't have permission to create student records."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:454
+msgid "A maximum of 10 applicants can be selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:463
+#. Default: ""
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:466
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:493
+#: waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:497
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:502
+msgid "Application record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:514
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Pre-fill container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:520
+msgid "Container must be in create mode to be pre-filled."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:527
+msgid "Pre-fill now"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:535
+msgid "%s application records created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:552
+#: waeup/kofa/applicants/viewlets.py:133
+msgid "Purge container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:556
+msgid "Remove initialized records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:557
+msgid "Don't use if application is in progress!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:558
+msgid "Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:568
+msgid "%s application records purged."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:585
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:625
+#. Default: ""
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional admission into the ${c} Academic Session of ${d}. Your student record has been created for you. Please, logout again and proceed to the login page of the portal. Then enter your new student credentials: user name= ${a}, password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:647
+#: waeup/kofa/students/browser.py:462
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:648
+#: waeup/kofa/students/browser.py:463
+msgid "unset"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:653
+#. Default: ""
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:707
+msgid "Student Record Creation Report"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:74
+msgid "You can not edit your application records after final submission. You really want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:766
+#: waeup/kofa/students/utils.py:511
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:781
+#: waeup/kofa/students/browser.py:2038
+#: waeup/kofa/students/browser.py:2077
+#: waeup/kofa/students/browser.py:2112
+msgid "Payment ticket created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:804
+#: waeup/kofa/students/browser.py:2137
+#. Default: ""
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:82
+msgid "Applicants Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:849
+#: waeup/kofa/students/browser.py:2195
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:855
+#: waeup/kofa/students/browser.py:2201
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:863
+msgid "Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:889
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:900
+msgid "Your image file is corrupted. Please replace."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:923
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:928
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:936
+msgid "Image file cannot be changed."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:949
+#: waeup/kofa/applicants/browser.py:1157
+#: waeup/kofa/applicants/browser.py:1160
+#: waeup/kofa/applicants/browser.py:1245
+msgid "Finally Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:950
+#: waeup/kofa/applicants/browser.py:1095
+#: waeup/kofa/applicants/browser.py:1153
+#: waeup/kofa/applicants/browser.py:1158
+#: waeup/kofa/applicants/browser.py:1194
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:950
+#: waeup/kofa/applicants/browser.py:1100
+#: waeup/kofa/applicants/browser.py:1149
+#: waeup/kofa/applicants/browser.py:1153
+#: waeup/kofa/applicants/browser.py:1158
+#: waeup/kofa/applicants/browser.py:1161
+#: waeup/kofa/applicants/browser.py:1199
+#: waeup/kofa/students/browser.py:1644
+#: waeup/kofa/students/browser.py:1686
+#: waeup/kofa/students/browser.py:1958
+#: waeup/kofa/students/browser.py:2998
+msgid "Remove selected tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:14
+msgid "Lastname (Surname):"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:34
+#. Default: "<strong>Congratulations!</strong> You have been offered provisional admission into the ${entry_session} Academic Session of ${app_title}."
+msgid "app_admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:6
+msgid "Registration Number or Applicant Id:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:74
+#. Default: "Your student record has already been created for you. Your new user name (= student id) is: <strong>${student_id}</strong>. Your initial password is: <strong>${appl_number}</strong>. Please proceed to the ${login_url}, enter your new login credentials and change your password immediately after login."
+msgid "appl_created"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:81
+msgid "Sorry, you have not been offered admission."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:87
+msgid "Your submitted application is being processed."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:93
+msgid "You have not yet submitted your application."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:24
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+msgid "Referee"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:118
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:42
+msgid "Payment Tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:160
+msgid "<strong>Notice:</strong> If you need to pay for another service, select the corresponding special application category above, click 'Save' and then 'Add online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:75
+msgid "I confirm that the Passport Photograph uploaded on this form is a true picture of me."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:92
+msgid "Referee Reports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+msgid "Report Id"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:13
+msgid "Your application record will be looked up and an email with your login credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:9
+msgid "An account will be created for you and an email with your login credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+msgid "First Choice"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:95
+#. Default: "There are more than 1000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:96
+msgid "search form"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Application Deadline"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/createallstudentspage.pt:10
+msgid "Failed records:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/createallstudentspage.pt:3
+msgid "Successfully created records:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:17
+msgid "Pre-fill '${code}' container with"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:20
+msgid "10 empty records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:21
+msgid "100 empty records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:22
+msgid "1000 empty records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:23
+msgid "10000 empty records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/prefillcontainer.pt:5
+#. Default: "<strong>Technical Note:</strong> An application in create mode can be performed with an empty container. Each time an applicant registers, a new record is being created and a corresponding user account set up. Since each single account creation causes a significant growth of Kofa's database, it is strongly recommended to pre-fill applicants containers with empty application records. This is done in a single transaction and does thus save database volume when application is ongoing."
+msgid "prefill"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/purgecontainer.pt:5
+#. Default: "<strong>Technical Note:</strong> During application, many records are being initialized but not used. For various reasons, many applicants do create one or more application records with corresponding user accounts but fail to log in. These records remain in state 'initialized' and are never turned to state 'started'. After a while, these records can be safely removed. <br /><br /> <strong>Attention:</strong> When purging an applicants container, also all unused pre-filled application records are being removed. If necessary, the container must be pre-filled with empty records again."
+msgid "purge"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/refereereportpage.pt:3
+#. Default: "You have been invited to report about ${applicant_name}. After submission the form, will be blocked. You can neither view nor edit your report after submission."
+msgid "referee_welcome"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:167
+msgid "Applicant Payments"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:146
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:147
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:180
+#: waeup/kofa/applicants/interfaces.py:247
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:201
+#: waeup/kofa/students/browser.py:1429
+#: waeup/kofa/students/browser.py:1595
+#: waeup/kofa/university/interfaces.py:41
+#: waeup/kofa/university/interfaces.py:84
+#: waeup/kofa/university/interfaces.py:152
+#: waeup/kofa/university/interfaces.py:223
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:207
+#: waeup/kofa/students/browser.py:1430
+#: waeup/kofa/students/browser.py:1596
+#: waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/university/interfaces.py:230
+#: waeup/kofa/browser/templates/source.pt:23
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:213
+#: waeup/kofa/applicants/interfaces.py:315
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:220
+#: waeup/kofa/applicants/interfaces.py:322
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:227
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:241
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:260
+#: waeup/kofa/applicants/interfaces.py:266
+#: waeup/kofa/hostels/interfaces.py:50
+#: waeup/kofa/hostels/interfaces.py:56
+#: waeup/kofa/interfaces.py:851
+msgid "Example: "
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:270
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:282
+msgid "Human readable notice on application slip in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:288
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:294
+msgid "With passport picture"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:329
+msgid "Container Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:331
+msgid "If set, this number will be added to the container prefix (e.g. app3). If not set, the year of entrance will be used (e.g. app2019)."
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/interfaces.py:637
+#: waeup/kofa/students/interfaces.py:231
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:371
+#: waeup/kofa/applicants/interfaces.py:517
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:377
+#: waeup/kofa/students/interfaces.py:279
+#: waeup/kofa/students/interfaces.py:417
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:523
+#: waeup/kofa/students/interfaces.py:258
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:389
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:263
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/applicants/interfaces.py:533
+#: waeup/kofa/applicants/interfaces.py:640
+#: waeup/kofa/students/interfaces.py:268
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:546
+#: waeup/kofa/students/interfaces.py:363
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:405
+#: waeup/kofa/students/interfaces.py:273
+msgid "Gender"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:411
+#: waeup/kofa/applicants/interfaces.py:553
+#: waeup/kofa/applicants/interfaces.py:583
+#: waeup/kofa/applicants/interfaces.py:645
+#: waeup/kofa/applicants/interfaces.py:684
+#: waeup/kofa/browser/interfaces.py:186
+#: waeup/kofa/interfaces.py:293
+#: waeup/kofa/interfaces.py:540
+#: waeup/kofa/interfaces.py:618
+#: waeup/kofa/students/interfaces.py:440
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:417
+#: waeup/kofa/applicants/interfaces.py:559
+#: waeup/kofa/applicants/interfaces.py:690
+#: waeup/kofa/interfaces.py:545
+#: waeup/kofa/interfaces.py:625
+#: waeup/kofa/students/interfaces.py:305
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:423
+#: waeup/kofa/applicants/interfaces.py:589
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/applicants/interfaces.py:595
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:608
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:439
+#: waeup/kofa/students/interfaces.py:253
+#: waeup/kofa/students/browser_templates/containerpage.pt:44
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:9
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:40
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:444
+#: waeup/kofa/applicants/interfaces.py:601
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:449
+#: waeup/kofa/applicants/interfaces.py:511
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:455
+#: waeup/kofa/applicants/interfaces.py:565
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:466
+msgid "School Grades"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:473
+msgid "Referees"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:538
+msgid "Identification Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:673
+#: waeup/kofa/payments/interfaces.py:100
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:679
+#: waeup/kofa/interfaces.py:287
+#: waeup/kofa/interfaces.py:289
+#: waeup/kofa/students/browser.py:3790
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:45
+#: waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt:37
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:48
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:44
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:94
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:696
+msgid "Report"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:56
+#: waeup/kofa/applicants/workflow.py:78
+#: waeup/kofa/students/payments.py:161
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:72
+msgid "Payment successfully completed. Kindly submit application for processing."
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:80
+#: waeup/kofa/students/payments.py:169
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:105
+#. Default: ""
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: user name= ${a}, password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:71
+msgid "Application Record"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:46
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:47
+msgid "Application Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:60
+#: waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:458
+#: waeup/kofa/students/utils.py:469
+#: waeup/kofa/students/utils.py:538
+#: waeup/kofa/students/utils.py:1114
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:70
+#: waeup/kofa/applicants/utils.py:76
+#: waeup/kofa/students/utils.py:509
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:142
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:155
+msgid "Manage container"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:165
+msgid "Export application data"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:169
+msgid "'The creation process may take a considerably long time depending on the number of student records to be created (appr. 0.5s per student). All users will be logged out and the portal will be in maintenance mode during record creation. You will be automatically logged in when the creation process is finished. Do you really want to start now?'"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:184
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:217
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:225
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:244
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:261
+#: waeup/kofa/applicants/workflow.py:119
+#: waeup/kofa/students/browser.py:366
+#: waeup/kofa/students/workflow.py:46
+msgid "Create student"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:278
+#: waeup/kofa/students/viewlets.py:521
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:293
+#: waeup/kofa/applicants/workflow.py:77
+#: waeup/kofa/students/viewlets.py:537
+#: waeup/kofa/students/workflow.py:117
+msgid "Approve payment"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:74
+#: waeup/kofa/students/viewlets.py:896
+msgid "My Data"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:105
+msgid "Process application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:106
+msgid "Application processed"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:120
+msgid "Student record created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:126
+#: waeup/kofa/applicants/workflow.py:140
+#: waeup/kofa/applicants/workflow.py:147
+#: waeup/kofa/applicants/workflow.py:154
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:127
+#: waeup/kofa/applicants/workflow.py:141
+#: waeup/kofa/applicants/workflow.py:148
+#: waeup/kofa/applicants/workflow.py:155
+msgid "Application reset"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:134
+msgid "Application reset to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:43
+msgid "started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45
+msgid "submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:46
+#: waeup/kofa/interfaces.py:98
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:47
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:48
+#: waeup/kofa/documents/utils.py:33
+#: waeup/kofa/interfaces.py:97
+msgid "created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:49
+msgid "processed"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:55
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:58
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:63
+msgid "Start application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:64
+msgid "Application started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:70
+msgid "Pay application fee"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:71
+msgid "Payment made"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:84
+msgid "Submit application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:85
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:91
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:92
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:98
+#: waeup/kofa/applicants/workflow.py:112
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:99
+#: waeup/kofa/applicants/workflow.py:113
+msgid "Application refused"
+msgstr ""
+
+#: waeup/kofa/authentication.py:412
+msgid "Password must have at least 6 characters."
+msgstr ""
+
+#: waeup/kofa/authentication.py:415
+#: waeup/kofa/authentication.py:437
+msgid "Passwords do not match."
+msgstr ""
+
+#: waeup/kofa/authentication.py:432
+msgid "Passwords must be at least 8 characters long, must contain at least one uppercase letter, one lowercase letter and one digit."
+msgstr ""
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:120
+#: waeup/kofa/browser/viewlets.py:823
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127
+#: waeup/kofa/browser/pages.py:790
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134
+#: waeup/kofa/browser/viewlets.py:320
+#: waeup/kofa/browser/viewlets.py:874
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152
+#: waeup/kofa/browser/pages.py:837
+#: waeup/kofa/browser/viewlets.py:329
+#: waeup/kofa/browser/viewlets.py:884
+msgid "Officers"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159
+#: waeup/kofa/browser/pages.py:1294
+#: waeup/kofa/browser/pages.py:1879
+#: waeup/kofa/browser/viewlets.py:338
+#: waeup/kofa/browser/viewlets.py:893
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166
+#: waeup/kofa/browser/reports.py:43
+#: waeup/kofa/browser/viewlets.py:347
+#: waeup/kofa/browser/viewlets.py:902
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174
+#: waeup/kofa/students/browser.py:3838
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:108
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:109
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:133
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:134
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:168
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:44
+#. Default: ""
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:59
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:71
+msgid "Could not determine file type."
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:76
+#. Default: ""
+msgid "${a} file format expected."
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:82
+#. Default: ""
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/fileviewlets.py:92
+#. Default: ""
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid "User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:102
+#: waeup/kofa/documents/browser.py:261
+#: waeup/kofa/students/browser.py:766
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1012
+#: waeup/kofa/browser/viewlets.py:411
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1025
+#: waeup/kofa/browser/viewlets.py:426
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052
+#: waeup/kofa/browser/viewlets.py:356
+msgid "My Courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1062
+msgid "no academic session set"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1083
+#: waeup/kofa/browser/pages.py:1111
+#: waeup/kofa/browser/viewlets.py:375
+#: waeup/kofa/browser/viewlets.py:394
+msgid "My ToDoList"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:109
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1138
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1152
+msgid "No object found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1168
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169
+#: waeup/kofa/browser/pages.py:1212
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169
+#: waeup/kofa/browser/pages.py:1222
+msgid "Purge mandates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1171
+#: waeup/kofa/browser/pages.py:1191
+#: waeup/kofa/browser/pages.py:1236
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1203
+#. Default: ""
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1213
+#: waeup/kofa/browser/pages.py:1223
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1214
+msgid "Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1219
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1227
+#. Default: ""
+msgid "${a} mandate(s) were purged."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1240
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1251
+msgid "The session chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1273
+#. Default: ""
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:130
+#. Default: ""
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1315
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1319
+#. Default: ""
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1330
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1332
+#: waeup/kofa/browser/pages.py:1348
+#: waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1864
+#: waeup/kofa/browser/pages.py:1882
+#: waeup/kofa/browser/pages.py:1925
+#: waeup/kofa/browser/pages.py:1969
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1344
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1347
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1388
+msgid "Import Manager"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1390
+#. Default: ""
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1394
+#. Default: ""
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1413
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1415
+#: waeup/kofa/browser/pages.py:2834
+#: waeup/kofa/students/browser.py:521
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1423
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:143
+#: waeup/kofa/browser/pages.py:1304
+#: waeup/kofa/hostels/browser.py:372
+#: waeup/kofa/hostels/browser.py:406
+#: waeup/kofa/students/browser.py:338
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1438
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1446
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1458
+msgid "The data in your file contain trailing whitespaces. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1462
+msgid "Your file contains forbidden characters or has invalid CSV format. First problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1568
+#: waeup/kofa/browser/pages.py:1672
+#: waeup/kofa/browser/pages.py:1785
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1549
+msgid "Portal must be in maintenance mode for data import."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1571
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1616
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1624
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1637
+#: waeup/kofa/browser/pages.py:1761
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1649
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1675
+#: waeup/kofa/browser/pages.py:1924
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1676
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1677
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1678
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1744
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1787
+msgid "Process next"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1803
+msgid "File has not been imported. Portal must be in maintenance mode for data import."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1849
+#: waeup/kofa/browser/pages.py:2058
+msgid "Maintenance mode disabled."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1852
+#. Default: ""
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1854
+#. Default: ""
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1862
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1865
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1883
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1895
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1910
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1913
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1921
+msgid "Edit data center settings"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1931
+msgid "You are not permitted to edit datacenter settings."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1947
+#. Default: ""
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1950
+#. Default: ""
+msgid "Given storage path cannot be used. ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1954
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1966
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1968
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2008
+msgid "Exports successfully purged."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2064
+msgid "Maintenance mode enabled."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2081
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2093
+#: waeup/kofa/browser/pages.py:2110
+#: waeup/kofa/browser/pages.py:2127
+#: waeup/kofa/browser/pages.py:2132
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2094
+#: waeup/kofa/students/reports/student_statistics.py:290
+#: waeup/kofa/university/export.py:37
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2099
+#: waeup/kofa/browser/viewlets.py:459
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2104
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2139
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2143
+#. Default: ""
+msgid "Faculty ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2167
+#: waeup/kofa/browser/pages.py:2178
+#: waeup/kofa/students/reports/student_statistics.py:290
+#: waeup/kofa/university/export.py:84
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2180
+#: waeup/kofa/browser/pages.py:2209
+#: waeup/kofa/browser/pages.py:2277
+#: waeup/kofa/browser/pages.py:2282
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2189
+#: waeup/kofa/browser/viewlets.py:487
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2194
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2229
+#: waeup/kofa/students/browser.py:262
+#: waeup/kofa/students/browser.py:304
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2234
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2268
+#: waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:336
+#: waeup/kofa/students/browser.py:3104
+#: waeup/kofa/students/browser.py:3405
+#: waeup/kofa/students/browser.py:3623
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2289
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2293
+#. Default: ""
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2315
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2345
+#: waeup/kofa/browser/pages.py:2376
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2345
+#: waeup/kofa/browser/pages.py:2392
+#: waeup/kofa/browser/pages.py:2421
+#: waeup/kofa/browser/pages.py:2426
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2346
+#: waeup/kofa/browser/pages.py:2382
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2346
+#: waeup/kofa/browser/pages.py:2397
+#: waeup/kofa/browser/pages.py:2463
+#: waeup/kofa/browser/pages.py:2468
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2356
+#: waeup/kofa/browser/viewlets.py:516
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2385
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2438
+msgid "A course with same code already exists: "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2444
+#. Default: ""
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2480
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2486
+#. Default: ""
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2525
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2584
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2592
+#: waeup/kofa/browser/pages.py:2610
+#: waeup/kofa/browser/pages.py:2637
+#: waeup/kofa/browser/pages.py:2640
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2593
+#: waeup/kofa/browser/pages.py:2605
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2599
+#: waeup/kofa/browser/viewlets.py:659
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2645
+#: waeup/kofa/browser/pages.py:2714
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2649
+#. Default: ""
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2697
+#: waeup/kofa/browser/viewlets.py:691
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2745
+#: waeup/kofa/students/browser.py:3156
+msgid "The portal is in maintenance mode. Password request forms are temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2765
+msgid "This is not a valid email address."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2774
+msgid "Send user a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2775
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2813
+#: waeup/kofa/applicants/browser_templates/createallstudentspage.pt:17
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2827
+#: waeup/kofa/students/browser.py:3233
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2831
+#. Default: ""
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:290
+msgid "Sorry, another export job is running. Please try again later."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:345
+#: waeup/kofa/browser/viewlets.py:267
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:355
+msgid "The portal is in maintenance mode and authentication (login) temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:361
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:387
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:416
+msgid "The portal is in maintenance mode. You can't login!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:434
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:440
+msgid "Your account has been temporarily deactivated because your parents have logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:449
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:468
+msgid "Your user name and password are correct but your password is not considered secure. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:472
+msgid " Your account has been temporarily deactivated. Please request a new password."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:479
+msgid "Your user name and password are correct but your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:484
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:499
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:541
+#. Default: ""
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:547
+#: waeup/kofa/browser/pages.py:985
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:566
+#: waeup/kofa/browser/pages.py:606
+#: waeup/kofa/browser/pages.py:1002
+#: waeup/kofa/students/browser.py:519
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:581
+msgid "The portal is in maintenance mode. The contact form is temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:593
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:608
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:633
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:744
+msgid "Sources & Vocabularies"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:758
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:838
+#: waeup/kofa/documents/viewlets.py:60
+#: waeup/kofa/documents/viewlets.py:79
+#: waeup/kofa/hostels/browser.py:308
+#: waeup/kofa/students/viewlets.py:194
+#: waeup/kofa/students/viewlets.py:285
+#: waeup/kofa/students/viewlets.py:364
+#: waeup/kofa/students/viewlets.py:382
+#: waeup/kofa/students/viewlets.py:413
+#: waeup/kofa/students/viewlets.py:511
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:839
+#: waeup/kofa/browser/viewlets.py:193
+msgid "Remove"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:847
+#. Default: ""
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:884
+#: waeup/kofa/browser/pages.py:886
+#: waeup/kofa/browser/viewlets.py:697
+msgid "Add officer"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:908
+#. Default: ""
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:932
+#. Default: ""
+msgid "Edit officer ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:966
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:983
+#: waeup/kofa/students/browser.py:500
+#. Default: ""
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:160
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:211
+#: waeup/kofa/browser/pdf.py:476
+#: waeup/kofa/browser/pdf.py:486
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:537
+#. Default: ""
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:807
+#. Default: ""
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:142
+msgid "Create report"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:147
+msgid "Currently, new reports cannot be created due to high system load. Please try again later."
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:90
+msgid "Unknown"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+msgid "Course Category:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:34
+msgid "Required course:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Course Cat."
+msgstr ""
+
+#: waeup/kofa/browser/templates/changepw.pt:10
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+msgid "email_address_problem"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:24
+msgid "expired"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid "Here you can create CSV files from parts of portal data except student data. Please pick the type of objects you want to export from the selection below. The file will be generated and then be made available for you to download in the table below. Files can only be downloaded within 24 hours after generation. The download link disappears afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:40
+msgid "Student data exports have to be further configured. You can either refine the results by <a href=\"@@exportconfig\">selecting certain export parameters</a> or by <a href=\"@@exportselected\">entering a set of student ids or matric numbers</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:88
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid "Find below the last ${count} imports. The files contain the imported datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid "Using batch processing you can mass-create, mass-update, or mass-remove datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid "Please select a file-processor and a processing-mode from the selections below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid "The data center helps you to manage portal data. You can upload CSV files here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid "Before uploading a file, check that your file header corresponds with the header format of the selected processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:19
+msgid "Many importer fields are of type 'Choice' which means only definied keywords are allowed:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:24
+msgid "View choices"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:41
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:57
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:6
+msgid "View available processors (importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:92
+msgid "After file upload click the 'Process data' button and proceed up to import step 3. Verify that the data format meets all the import criteria and requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:98
+msgid "Import managers will be automatically informed by email after file upload."
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:25
+msgid "Select new file&hellip; ${input_name}"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+msgid "Password"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:20
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+msgid "login_trouble1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:23
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+msgid "login_trouble2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:28
+#: waeup/kofa/browser/templates/loginpage.pt:35
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+msgid "login_trouble4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:32
+#. Default: "Or simply forgot your student id, applicant id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+msgid "User Name or Id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/mycourses.pt:2
+msgid "Academic Session: <span>${DYNAMIC_CONTENT}</span>"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:1
+msgid "Transcripts to be validated"
+msgstr ""
+
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:12
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:43
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:32
+msgid "Transcripts to be signed and released"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:29
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:30
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:31
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:32
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:39
+msgid "Download CSV File Template"
+msgstr ""
+
+#: waeup/kofa/browser/templates/processorsoverview.pt:84
+msgid "Close"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid "Here you can create reports representing portal data. Please pick the type of report you want to create from the selection below. Afterwards you can configure the report (do the settings). The report data will be calculated and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:52
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/source.pt:22
+msgid "Value"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:148
+msgid "Kofa Docs for this page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:68
+msgid "M A I N T E N A N C E &nbsp;&nbsp;&nbsp; M O D E &nbsp;&nbsp;&nbsp; O N"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:1
+#. Default: "<strong>ATTENTION:</strong> This account has been suspended. The officer can't login."
+msgid "officer_suspended_warning"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1015
+msgid "Academic Sessions"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1023
+msgid "Course Levels"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1031
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1039
+msgid "Study Modes"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1047
+msgid "Degrees"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1055
+#: waeup/kofa/students/browser_templates/exportconfig.pt:71
+msgid "Payment Categories"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1063
+msgid "Application Types (Prefixes)"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1071
+msgid "Application Categories"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1081
+msgid "Student Registration States"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1091
+msgid "Verdicts"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1101
+msgid "Exam Subjects"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:111
+#: waeup/kofa/browser/viewlets.py:173
+#: waeup/kofa/students/browser.py:1229
+#: waeup/kofa/students/viewlets.py:373
+#: waeup/kofa/students/viewlets.py:572
+#: waeup/kofa/students/viewlets.py:625
+msgid "Edit"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1111
+msgid "Exam Grades"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1121
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:1131
+msgid "Disable Payment Groups"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:183
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:306
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:442
+#: waeup/kofa/students/viewlets.py:185
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:451
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:469
+#: waeup/kofa/browser/viewlets.py:508
+#: waeup/kofa/browser/viewlets.py:539
+#: waeup/kofa/browser/viewlets.py:588
+#: waeup/kofa/browser/viewlets.py:682
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:478
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:495
+#: waeup/kofa/students/browser.py:261
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:527
+#: waeup/kofa/browser/viewlets.py:576
+#: waeup/kofa/browser/viewlets.py:670
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:551
+msgid "Clear all students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:557
+msgid "'All students, who requested clearance in this department, will be cleared. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:616
+#. Default: ""
+msgid "Update session ${a} scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:626
+msgid "Download course tickets overview"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:638
+msgid "Download attendance sheet"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:650
+msgid "Download csv file (editable scores only)"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:706
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:715
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:724
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:733
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:742
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:751
+msgid "Switch maintenance mode"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:757
+msgid "'In maintenance mode no other user can login, and already logged-in users will be automatically logged out. You will be the only logged-in user and you can safely start any import. Please wait a few seconds before starting the import so that all running processes are finished. If the import is done, maintenance mode will be automatically disabled. \\n\\nYou really want to enable maintenance mode?'"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:838
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:992
+msgid "Institution Types"
+msgstr ""
+
+#: waeup/kofa/documents/batching.py:143
+msgid "Public PDF Document Processor"
+msgstr ""
+
+#: waeup/kofa/documents/batching.py:155
+msgid "Public HTML Document Processor"
+msgstr ""
+
+#: waeup/kofa/documents/batching.py:167
+msgid "Public REST Document Processor"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:102
+msgid "Remove selected documents"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:141
+msgid "The id chosen already exists."
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:145
+#. Default: ""
+msgid "${a} added."
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:212
+#: waeup/kofa/documents/fileviewlets.py:90
+msgid "The document requested has not yet been published."
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:269
+msgid "Manage document "
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:354
+msgid "Trigger document transition"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:372
+msgid "Apply now"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:380
+msgid "Transition '%s' executed."
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:62
+#: waeup/kofa/documents/browser.py:82
+#: waeup/kofa/documents/viewlets.py:49
+#: waeup/kofa/documents/browser_templates/containermanagepage.pt:3
+msgid "Documents"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:95
+msgid "Manage document section"
+msgstr ""
+
+#: waeup/kofa/documents/browser.py:97
+#: waeup/kofa/documents/browser.py:120
+#: waeup/kofa/documents/browser.py:130
+msgid "Add document"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/containerpage.pt:1
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/documentaddform.pt:7
+msgid "Document Type"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:9
+msgid "Files"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/documentpage.pt:16
+#: waeup/kofa/documents/browser_templates/htmldocumentpage.pt:16
+msgid "Document Type:"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/documentpage.pt:25
+msgid "Connected Files"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt:39
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:50
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:115
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt:6
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:6
+msgid "Metadata"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt:9
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/htmldocumentpage.pt:25
+msgid "Content:"
+msgstr ""
+
+#: waeup/kofa/documents/browser_templates/htmldocumentpage.pt:33
+msgid "HTML Element:"
+msgstr ""
+
+#: waeup/kofa/documents/export.py:102
+msgid "Public PDF Documents"
+msgstr ""
+
+#: waeup/kofa/documents/export.py:111
+msgid "Public HTML Documents"
+msgstr ""
+
+#: waeup/kofa/documents/export.py:120
+msgid "Public REST Documents"
+msgstr ""
+
+#: waeup/kofa/documents/fileviewlets.py:43
+#: waeup/kofa/documents/fileviewlets.py:44
+msgid "PDF File"
+msgstr ""
+
+#: waeup/kofa/documents/fileviewlets.py:64
+#: waeup/kofa/documents/fileviewlets.py:65
+msgid "PDF Scan"
+msgstr ""
+
+#: waeup/kofa/documents/interfaces.py:50
+msgid "Document Id"
+msgstr ""
+
+#: waeup/kofa/documents/interfaces.py:56
+msgid "Document Title"
+msgstr ""
+
+#: waeup/kofa/documents/interfaces.py:82
+msgid "Multilingual content in HTML format"
+msgstr ""
+
+#: waeup/kofa/documents/interfaces.py:95
+msgid "Multilingual content in REST (reStructuredText) format"
+msgstr ""
+
+#: waeup/kofa/documents/utils.py:34
+msgid "published"
+msgstr ""
+
+#: waeup/kofa/documents/utils.py:38
+msgid "PDF Document"
+msgstr ""
+
+#: waeup/kofa/documents/utils.py:39
+msgid "HTML Document"
+msgstr ""
+
+#: waeup/kofa/documents/utils.py:40
+msgid "REST Document"
+msgstr ""
+
+#: waeup/kofa/documents/viewlets.py:69
+#: waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:355
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/documents/viewlets.py:89
+msgid "Transition"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:39
+msgid "Create document"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:42
+msgid "Document created"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:47
+msgid "Publish document"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:50
+msgid "Document published"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:55
+msgid "Retract documet"
+msgstr ""
+
+#: waeup/kofa/documents/workflow.py:58
+msgid "Document retracted"
+msgstr ""
+
+#: waeup/kofa/hostels/batching.py:42
+msgid "Hostel Processor"
+msgstr ""
+
+#: waeup/kofa/hostels/batching.py:95
+msgid "Bed Processor (update only)"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:102
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:115
+#: waeup/kofa/hostels/browser.py:161
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:123
+msgid "Bed statistics"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133
+msgid "Release all expired bed space allocations"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:143
+msgid "'All expired bed space allocations will be annulled. Are you sure? \\n\\nBed space allocation expires ${a} days after accommodation booking if maintenance fee is not paid.'"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:164
+#: waeup/kofa/hostels/browser.py:186
+#: waeup/kofa/hostels/browser.py:269
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:165
+#: waeup/kofa/hostels/browser.py:191
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:194
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:205
+msgid "You can't change the booking session before clearing all hostels."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:223
+msgid "Forbidden"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Portal must be in maintenance mode for releasing expired bed allocations."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:238
+#. Default: ""
+msgid "Successfully released ${a} beds."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:418
+#. Default: ""
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244
+msgid "No bed released."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:259
+msgid "Bed Statistics"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:273
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:282
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:284
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:319
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:322
+#: waeup/kofa/hostels/browser.py:338
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:323
+#: waeup/kofa/hostels/browser.py:366
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:324
+#: waeup/kofa/hostels/browser.py:400
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:325
+#: waeup/kofa/hostels/browser.py:427
+msgid "Clear hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:339
+msgid "Attention: The updater removes all reservation flags of existing beds. You really want to update?"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:344
+msgid "Portal must be in maintenance mode for bed updates."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:352
+#. Default: ""
+msgid "${a} empty beds removed, ${b} beds added, ${c} occupied beds modified"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:357
+#. Default: ""
+msgid "${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:391
+#. Default: ""
+msgid "Successfully switched ${a} beds."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:394
+#. Default: ""
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:423
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:430
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:443
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:450
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:65
+#: waeup/kofa/hostels/browser.py:75
+#: waeup/kofa/hostels/export.py:35
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:92
+#. Default: ""
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:40
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/documents/browser_templates/containerpage.pt:8
+#: waeup/kofa/documents/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:24
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerstatistics.pt:6
+msgid "Occupied"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerstatistics.pt:7
+msgid "Free"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerstatistics.pt:8
+msgid "Total"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:41
+#: waeup/kofa/documents/browser_templates/containerpage.pt:10
+#: waeup/kofa/documents/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:42
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:43
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:61
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:73
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:81
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:58
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:222
+msgid "unreserved"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:225
+msgid "reserved"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:114
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:118
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:125
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:131
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:137
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:143
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:151
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:159
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:167
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:175
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:191
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:199
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:206
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:216
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:259
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:265
+#: waeup/kofa/hostels/browser_templates/containerstatistics.pt:5
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:48
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:271
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:276
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:277
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:292
+msgid "Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:299
+#. Default: ""
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:48
+msgid "Hostel Allocation Start Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:54
+msgid "Hostel Allocation Closing Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:60
+msgid "Booking Session"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:68
+msgid "Allowed States"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:76
+msgid "Allocation Expiration Time (days)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:77
+msgid "Number of days after which allocation is being annulled"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:47
+msgid "Bed J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:48
+msgid "Bed K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:49
+msgid "Bed L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:62
+msgid "Block J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:75
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:100
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:104
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:105
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:106
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:107
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:108
+msgid "transcript validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:109
+msgid "transcript released"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:265
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:270
+#: waeup/kofa/students/browser.py:1433
+#: waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:297
+msgid "Email Address (referees will be automatically invited by email after final submission of this form)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:472
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:498
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:505
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:512
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:516
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:520
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:550
+#: waeup/kofa/interfaces.py:608
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:562
+#: waeup/kofa/interfaces.py:563
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:569
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:570
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:599
+#: waeup/kofa/documents/browser_templates/htmldocumentmanagepage.pt:35
+#: waeup/kofa/documents/browser_templates/documentmanagepage.pt:46
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:600
+msgid "Login name of user"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:604
+#: waeup/kofa/students/browser_templates/containerpage.pt:48
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:609
+msgid "Substitute for officer's real name in student object histories."
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:631
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:638
+msgid "If set, the account is immediately blocked."
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:69
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:22
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:697
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:698
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:70
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:703
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:709
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:71
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:722
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:728
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:735
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:736
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:741
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:748
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:755
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:760
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:761
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:769
+msgid "Next Matriculation Number Integer"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:770
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:778
+msgid "Next Matriculation Number Integer 2"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:779
+msgid "2nd integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:787
+msgid "Next Matriculation Number Integer 3"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:788
+msgid "3rd integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:796
+msgid "Next Matriculation Number Integer 4"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:797
+msgid "4th integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:805
+msgid "Export-disabled message"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:806
+msgid "Message which will show up if an officer tries to export data. All exporters are automatcally disabled if this field is set."
+msgstr ""
+
+#: waeup/kofa/interfaces.py:813
+msgid "Maintenance Mode enabled by"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:827
+#: waeup/kofa/interfaces.py:900
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:835
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:840
+msgid "Payment disabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:849
+msgid "Course Reg. Deadline"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:855
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:861
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:867
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:873
+msgid "Late Course Reg. Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:879
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:885
+msgid "Transfer Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:37
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:103
+msgid "Parents password has been successfully set. Login with your new parents password."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:52
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+#: waeup/kofa/mandates/mandate.py:98
+#: waeup/kofa/mandates/mandate.py:127
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:73
+#: waeup/kofa/mandates/mandate.py:101
+#: waeup/kofa/mandates/mandate.py:122
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:75
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:106
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:55
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:128
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:112
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:124
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:131
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:138
+msgid "Response Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:145
+msgid "Response Description"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:29
+msgid "Fees waived"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:30
+msgid "Scholarship"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:59
+#: waeup/kofa/students/interfaces.py:877
+#: waeup/kofa/students/interfaces.py:900
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:72
+msgid "Combi Payment"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:81
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:87
+#: waeup/kofa/students/interfaces.py:884
+#: waeup/kofa/students/interfaces.py:907
+#: waeup/kofa/students/browser_templates/exportconfig.pt:85
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:93
+msgid "Payment State"
+msgstr ""
+
+#: waeup/kofa/payments/payment.py:96
+#. Default: ""
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:199
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:114
+msgid "Previous session bookings can't be changed."
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:120
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:125
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:150
+#: waeup/kofa/students/browser.py:2419
+#. Default: ""
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:162
+#: waeup/kofa/students/browser.py:2459
+#. Default: ""
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#: waeup/kofa/students/accommodation.py:170
+#. Default: ""
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:1014
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:467
+msgid "StudentStudyCourse Processor (update only)"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:568
+msgid "StudentStudyLevel Processor"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:664
+msgid "CourseTicket Processor"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:788
+msgid "StudentOnlinePayment Processor"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:82
+msgid "Student Processor"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:963
+msgid "Verdict Processor (special processor, update only)"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1021
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1047
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1052
+msgid "Level zero must not be assigned a session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1063
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1065
+#: waeup/kofa/students/browser.py:2916
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1075
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1099
+#: waeup/kofa/students/viewlets.py:756
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1109
+#: waeup/kofa/students/browser.py:2669
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1128
+#: waeup/kofa/students/browser.py:2396
+#: waeup/kofa/students/browser.py:2691
+#: waeup/kofa/students/browser.py:2840
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:2426
+#: waeup/kofa/students/browser.py:2694
+#: waeup/kofa/students/browser.py:2844
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1136
+#: waeup/kofa/students/browser.py:2435
+#: waeup/kofa/students/browser.py:2699
+#: waeup/kofa/students/browser.py:2848
+#: waeup/kofa/students/browser.py:3133
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1140
+#: waeup/kofa/students/browser.py:2441
+#: waeup/kofa/students/browser.py:2703
+#: waeup/kofa/students/browser.py:2853
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1164
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1184
+msgid "You have already signed this transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1188
+msgid "Transcript signed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1215
+#: waeup/kofa/students/viewlets.py:772
+#: waeup/kofa/students/workflow.py:231
+msgid "Validate transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1216
+msgid "Save comment and validate transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1260
+msgid "Transcript validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1286
+#: waeup/kofa/students/viewlets.py:788
+#: waeup/kofa/students/workflow.py:238
+msgid "Release transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1287
+msgid "Save comment and release transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1321
+msgid "Transcript released and final transcript file saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1354
+#: waeup/kofa/students/browser.py:1388
+msgid "Forbidden!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1368
+#. Default: ""
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1403
+#: waeup/kofa/students/viewlets.py:805
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1410
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1428
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:39
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1431
+#: waeup/kofa/students/interfaces.py:699
+#: waeup/kofa/university/interfaces.py:165
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1432
+#: waeup/kofa/students/browser.py:1601
+#: waeup/kofa/students/browser.py:3738
+#: waeup/kofa/students/interfaces.py:722
+#: waeup/kofa/students/interfaces.py:753
+#: waeup/kofa/students/browser_templates/editscorespage.pt:83
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:46
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1467
+#: waeup/kofa/students/viewlets.py:233
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1472
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1476
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1479
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1482
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1485
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1495
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1499
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1504
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1507
+msgid "Previous transfer reverted."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1539
+#. Default: ""
+msgid "${a}: ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1562
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1571
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1579
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1581
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1583
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1597
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1598
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:11
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:42
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1599
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1643
+#: waeup/kofa/students/browser.py:1851
+#: waeup/kofa/students/browser.py:1863
+#: waeup/kofa/students/browser.py:3073
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1645
+#: waeup/kofa/students/browser.py:2935
+msgid "Enter valid course code"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:165
+#. Default: ""
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:2948
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1661
+#: waeup/kofa/students/browser.py:2953
+msgid "Course not found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1678
+#. Default: ""
+msgid "Manage ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1692
+#: waeup/kofa/students/browser.py:2977
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1734
+#. Default: ""
+msgid "Edit transcript remark of level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1741
+msgid "Save remark and go and back to transcript validation page"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1761
+#: waeup/kofa/students/browser.py:1787
+msgid "This is not the student's current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1766
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1768
+#: waeup/kofa/students/browser.py:1800
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1793
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1797
+#: waeup/kofa/students/browser.py:1835
+msgid "Course list has been unregistered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1828
+msgid "Course registration has ended. Unregistration is disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1832
+msgid "This is not your current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1838
+msgid "You are in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:186
+#: waeup/kofa/students/export.py:294
+#: waeup/kofa/students/viewlets.py:851
+msgid "Students"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1888
+#. Default: ""
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1922
+#. Default: ""
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1955
+#. Default: ""
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1996
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2006
+#: waeup/kofa/students/browser.py:2064
+#: waeup/kofa/students/browser.py:2097
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2020
+#: waeup/kofa/students/utils.py:657
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2026
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2054
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2059
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2094
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:210
+#: waeup/kofa/students/viewlets.py:120
+#: waeup/kofa/students/viewlets.py:915
+#: waeup/kofa/students/viewlets.py:928
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:212
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:218
+#: waeup/kofa/students/viewlets.py:127
+#: waeup/kofa/students/viewlets.py:916
+#: waeup/kofa/students/viewlets.py:929
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2256
+#: waeup/kofa/students/browser.py:2257
+#: waeup/kofa/students/browser.py:2296
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2261
+#. Default: ""
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2267
+#: waeup/kofa/students/browser.py:2281
+msgid "No favoured hostel"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Your selection has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2304
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2312
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:233
+#: waeup/kofa/students/viewlets.py:135
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2337
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2339
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2364
+#. Default: ""
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:242
+#. Default: ""
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2468
+#. Default: ""
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2485
+#. Default: ""
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2505
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2514
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2525
+msgid ""
+"\n"
+"<br /><br /><br /><br /><br /><font size=\"12\">\n"
+"Please endeavour to pay your hostel maintenance charge within ${a} days\n"
+" of being allocated a space or else you are deemed to have\n"
+" voluntarily forfeited it and it goes back into circulation to be\n"
+" available for booking afresh!</font>)\n"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2574
+#. Default: ""
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2586
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2601
+#: waeup/kofa/students/viewlets.py:582
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2617
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2629
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2645
+#: waeup/kofa/students/viewlets.py:610
+#: waeup/kofa/students/workflow.py:68
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2649
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2655
+msgid "Email address is missing."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2657
+msgid "Phone number is missing."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2673
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2708
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2718
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2739
+#: waeup/kofa/students/browser.py:2755
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2747
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2748
+msgid "You can not edit your data after requesting clearance. You really want to request clearance now?"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+#: waeup/kofa/students/browser.py:2801
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2770
+#: waeup/kofa/students/browser.py:2805
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2779
+#: waeup/kofa/students/workflow.py:82
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2780
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2783
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2795
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2814
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2818
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2827
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:286
+#: waeup/kofa/students/browser.py:329
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2867
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2870
+msgid "Session started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2889
+#. Default: ""
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2901
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2907
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2920
+#: waeup/kofa/students/utils.py:652
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2923
+msgid "You successfully created a new course list."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2965
+#. Default: ""
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3019
+msgid "All course tickets updated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Update all tickets"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3023
+msgid "Update all course parameters including course titles."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:303
+#: waeup/kofa/students/viewlets.py:167
+msgid "Manage students section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3031
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3051
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3052
+msgid "You can not edit your course list after registration. You really want to register?"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3089
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3092
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3107
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3115
+msgid "Access code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3118
+#. Default: ""
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3124
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3130
+msgid "Access code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3139
+#. Default: ""
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3151
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3177
+#: waeup/kofa/students/browser.py:3208
+#: waeup/kofa/students/browser.py:3286
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3254
+msgid "Request password for parents access"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3262
+msgid "Send temporary login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3289
+msgid "Wrong email address."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3305
+msgid "You have successfully requested a parents password for the"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3329
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3345
+#: waeup/kofa/students/interfaces.py:475
+#: waeup/kofa/students/interfaces.py:551
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3346
+#: waeup/kofa/students/interfaces.py:482
+#: waeup/kofa/students/interfaces.py:521
+#: waeup/kofa/students/interfaces.py:557
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3396
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3402
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3439
+#: waeup/kofa/students/interfaces.py:596
+#: waeup/kofa/students/interfaces.py:804
+#: waeup/kofa/students/utils.py:273
+#: waeup/kofa/students/utils.py:278
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:45
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3440
+#: waeup/kofa/students/browser.py:3737
+#: waeup/kofa/students/browser.py:3791
+#: waeup/kofa/students/interfaces.py:589
+#: waeup/kofa/students/utils.py:280
+#: waeup/kofa/university/interfaces.py:353
+#: waeup/kofa/students/browser_templates/editscorespage.pt:82
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3481
+msgid "%d students have been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3537
+msgid "Uploaded file contains illegal data. Ignored"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3542
+msgid "No file provided."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3567
+msgid "Error: Score(s) of following students have not been updated (only integers are allowed): %s., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3604
+#: waeup/kofa/students/browser.py:3672
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3608
+#: waeup/kofa/students/browser.py:3676
+#: waeup/kofa/students/browser.py:3717
+#: waeup/kofa/students/browser.py:3773
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3614
+#: waeup/kofa/students/browser.py:3682
+msgid "Course results have already been validated and can no longer be changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:363
+#: waeup/kofa/students/viewlets.py:175
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3634
+msgid "Course results have already been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3638
+msgid "You successfully validated the course results."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3645
+msgid "You successfully updated course results."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:371
+msgid "Student record created."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3732
+#: waeup/kofa/students/browser.py:3789
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:8
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:39
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3733
+msgid "Reg. No."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3734
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:10
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:41
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3735
+#: waeup/kofa/students/browser_templates/editscorespage.pt:80
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:57
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3736
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:13
+#: waeup/kofa/browser/templates/mytranscriptrequests.pt:44
+msgid "Course of Studies"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:375
+msgid "Create graduated student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3788
+msgid "S/N"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3792
+msgid ""
+"Course of\n"
+"Studies"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3793
+msgid "Booklet No."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3794
+msgid "Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:384
+msgid "Graduated student record created."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3867
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3874
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3881
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3888
+msgid "All Semesters"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3895
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3902
+msgid "All Payment Categories"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3973
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:4021
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:404
+#. Default: ""
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:4071
+msgid "Export of selected students started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:4162
+msgid "Not permitted."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:4171
+msgid "Academic session not set. Please contact the administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:4200
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:426
+msgid "Login now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:430
+#. Default: ""
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:436
+msgid "You successfully logged in as student."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:454
+#. Default: ""
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:456
+#. Default: ""
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:539
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:557
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:599
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:639
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:658
+msgid "Student account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:691
+#. Default: ""
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:720
+#: waeup/kofa/students/viewlets.py:107
+#: waeup/kofa/students/viewlets.py:925
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:726
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:735
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:736
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:743
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:744
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:765
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:802
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:804
+#: waeup/kofa/students/browser.py:841
+#: waeup/kofa/students/browser.py:1177
+#: waeup/kofa/students/browser.py:1243
+#: waeup/kofa/students/browser.py:1304
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:816
+#: waeup/kofa/students/viewlets.py:314
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:830
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:834
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:838
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:871
+#. Default: ""
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:884
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:899
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:902
+msgid "Save/Confirm"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:929
+#. Default: ""
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:932
+#. Default: ""
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:974
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:976
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:981
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:992
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:995
+#: waeup/kofa/students/browser.py:1069
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:996
+#: waeup/kofa/students/browser.py:1042
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:33
+msgid "Favoured Hostel:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:38
+msgid "Bookings"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:47
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:6
+msgid "Select and <strong>save the desired hostel</strong> before creating a bed ticket. Your choice cannot be changed afterwards."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/bedticketaddpage.pt:12
+msgid "Desired Bed Space:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/bedticketaddpage.pt:6
+msgid "Select your bed before booking acommodation. Your choice cannot be changed afterwards."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:22
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "in state 'transcript requested' (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:22
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:22
+msgid "in state 'transcript validated' (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:25
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:25
+msgid "in state 'graduated' (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:45
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:46
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:47
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:18
+msgid "Select csv file&hellip; ${input_file}"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:26
+msgid "Help"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:41
+msgid "Step-by-step instructions"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:64
+#. Default: "<ol> <li>Download csv file.</li> <li>Open csv file in a text editor or in a spreadsheet programme (Excel or Calc).</li> <li>Edit course results only. Do not modify other entries. Do not remove or add columns. Do not add rows.</li> <li>Save file in same format (csv). Do not switch to any other format (xls, xlsx or ods).</li> <li>Select same file for upload and press the blue 'Update ...' button.</li> <li>The values in the table will be updated. Spot-check if the values in the table correspond with the values in your file.</li> <li>Ready</li> </ol> <strong>Note:</strong> Only course results of students which are in state 'courses validated' and in academic session ${current_academic_session} can be modified. Additional data will just be ignored."
+msgid "lecturer_help"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#. Default: "This page helps you to update your students' course results. You can either update scores by importing a csv file (press 'Help' for further information) or by changing score values in the table below. Only scores of students in state 'courses validated' and academic session ${current_academic_session} can be modified."
+msgid "edit_scores_info"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid "Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:109
+msgid "Course Ticket Session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:119
+msgid "Course Ticket Semester"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+#: waeup/kofa/students/browser_templates/exportselected.pt:9
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+msgid "Current Study Mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:99
+msgid "Course Ticket Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:82
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:53
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:54
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:55
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportselected.pt:1
+msgid "Here you can export student data for a small subset of students by entering a list of student ids or matric numbers. The values must be separated by whitespaces or commas. You can also copy and paste parts of Excel or Calc columns."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportselected.pt:19
+msgid "Student Ids or Matric Numbers"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+msgid "and"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+msgid "login_as_intro_1"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:129
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:130
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Item"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:126
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:56
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:127
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestppw.pt:16
+msgid "The student record of your child will be looked up and temporary login credentials will be sent to the email address given above. To be able to proceed you must enter the parents email address stored in Kofa. After login, you'll only be able to view certain data. You won't be able to edit data, register courses or make payments. The login credentials will be valid for ${minutes} minutes. After this time you'll be automatically logged out."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:9
+msgid "Your student record will be looked up and your login credentials will be sent to the email address given above. To be able to proceed you must provide a valid email address!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:32
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid "Please note that passwords are case-sensitive, <br />when entering your credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid "Student transfers can be reverted one after the other. When reverting a transfer, the active study course will be entirely removed including all containing course lists (study levels) and the previous study course will be reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid "<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) registration number and the access code. </li> <li> After pressing the button, your student id will be displayed and your password set. The 10-digit part of the access code just used is your new password. </li> <li> Go to the <strong><a href=\"login\">general login page</a></strong> and enter your new credentials. </li>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "In Kofa a study level is a container for courses, which means it can list the courses taken by the student at a certain level. The container also stores and provides the information when and by whom the course list was validated, in which session the level was taken and which verdict the student finally obtained."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:48
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:22
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:49
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "OS"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:66
+msgid "You don't know the course code? Then select a course <a href=\"ctadd\">here</a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:96
+msgid "You don't know the course code? Then select a course <a href=\"add\">here</a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:42
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:43
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:45
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:50
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:50
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:57
+msgid "CO: Carry-over course<br /> OS: Outstanding course<br /> Requ.: Required course<br /> Auto: Automatically created course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:7
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+msgid "Subjects/Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:16
+msgid "View signatures"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:21
+msgid "View remarks"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:32
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+#. Default: ""
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/bedticketaddpage.pt:29
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/bedticketaddpage.pt:46
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:320
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:351
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:385
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelremarkpage.pt:12
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Data for Lecturer"
+msgstr ""
+
+#: waeup/kofa/students/export.py:446
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:472
+msgid "Student Unpaid Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:499
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:535
+msgid "Accommodation Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:550
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:587
+msgid "Student School Fee Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:641
+msgid "Session Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:700
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:730
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/fileviewlets.py:108
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/fileviewlets.py:125
+#: waeup/kofa/students/fileviewlets.py:138
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/fileviewlets.py:126
+#: waeup/kofa/students/fileviewlets.py:139
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/kofa/students/fileviewlets.py:96
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:237
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Flash Notice"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:248
+msgid "This single-line message will be shown in a flash box."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:286
+#: waeup/kofa/students/interfaces.py:425
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:293
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:299
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:310
+msgid "Parents' Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:352
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:357
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:370
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:378
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:387
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:409
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:435
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:454
+#: waeup/kofa/students/interfaces.py:515
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:460
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:467
+#: waeup/kofa/students/interfaces.py:528
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:489
+#: waeup/kofa/students/interfaces.py:545
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:496
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:563
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:568
+#: waeup/kofa/students/interfaces.py:609
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:615
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:621
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:627
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:633
+msgid "Transcript Remark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:672
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:677
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:682
+#: waeup/kofa/university/interfaces.py:177
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:688
+msgid "Imported Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:694
+#: waeup/kofa/university/interfaces.py:171
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:704
+#: waeup/kofa/browser/templates/certificatepage.pt:34
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:44
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:710
+msgid "Outstanding Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:716
+#: waeup/kofa/university/interfaces.py:360
+msgid "Course Category"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:729
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:735
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:744
+#: waeup/kofa/university/interfaces.py:348
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:759
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:770
+msgid "Desired Hostel"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:786
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:798
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:811
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:46
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:817
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:841
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:847
+#: waeup/kofa/students/interfaces.py:890
+#: waeup/kofa/students/interfaces.py:913
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:919
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:923
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:149
+msgid "Payment successfully completed"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid "Please pick the certificate, session and level for which the report should be generated. The report will be processed and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+#: waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt:2
+msgid "Please pick the session and study modes group for which the report should be generated. The report data will be calculated and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:20
+#: waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt:20
+msgid "Study Modes Group"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+#: waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt:9
+msgid "Breakdown"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:133
+#: waeup/kofa/students/reports/level_report.py:291
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:234
+#: waeup/kofa/students/reports/raw_score_report.py:220
+#: waeup/kofa/students/reports/session_results_presentation.py:173
+msgid "All Certificates"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:317
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:333
+msgid "No certificate selected."
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:352
+#: waeup/kofa/students/reports/student_payment_statistics.py:338
+#: waeup/kofa/students/reports/student_statistics.py:256
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:67
+#: waeup/kofa/students/reports/session_results_presentation.py:235
+msgid "Student Name"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:68
+msgid "Total Credits Taken"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:69
+msgid "Total Credits Passed"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:70
+msgid "GPA"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:71
+msgid "Courses Failed"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:72
+msgid "Outstanding Courses"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:73
+msgid "Cum. Credits Taken"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:74
+msgid "Cum. Credits Passed"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:75
+msgid "CGPA"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:76
+msgid "Remark"
+msgstr ""
+
+#: waeup/kofa/students/reports/raw_score_report.py:130
+msgid "Raw Score Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/raw_score_report.py:294
+msgid "RAW Score Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/raw_score_report.py:317
+msgid "Create raw score report"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:180
+msgid "all levels"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:280
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:303
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:78
+msgid "Presentation of Session Results"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_payment_statistics.py:197
+#: waeup/kofa/students/reports/student_payment_statistics.py:201
+#: waeup/kofa/students/reports/student_payment_statistics.py:283
+msgid "Student Payment Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_payment_statistics.py:273
+#. Default: ""
+msgid "Student Payments - ${a} - ${b} - ${c} - "
+msgstr ""
+
+#: waeup/kofa/students/reports/student_payment_statistics.py:317
+msgid "Create student payment statistics report"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:146
+#: waeup/kofa/students/reports/student_statistics.py:150
+#: waeup/kofa/students/reports/student_statistics.py:204
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:194
+#. Default: ""
+msgid "${a} Students - ${b} -"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:231
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/studylevel.py:235
+msgid "Course registration has ended. Please pay the late registration fee."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1008
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1030
+#. Default: ""
+msgid "<b>Lecturer(s): ${a}</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1033
+#. Default: ""
+msgid "<b>Credits: ${a}</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1038
+#: waeup/kofa/students/utils.py:1041
+#. Default: ""
+msgid "<b>${a}</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1045
+#. Default: ""
+msgid "<b>Total Students: ${a}</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1048
+#. Default: ""
+msgid "<b>Total Pass: ${a} (${b}%)</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1051
+#. Default: ""
+msgid "<b>Total Fail: ${a} (${b}%)</b>"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1063
+#. Default: ""
+msgid ""
+"${a} (${b})\n"
+"Academic Session ${d}"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1068
+msgid "Attendance Sheet"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1070
+msgid "Course Tickets Overview"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1075
+#. Default: ""
+msgid "${b} - ${d}"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1094
+#: waeup/kofa/students/utils.py:1096
+msgid "Maximum credits exceeded."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:1116
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:133
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:39
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:152
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:45
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:159
+#: waeup/kofa/students/utils.py:946
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:51
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:168
+#: waeup/kofa/students/utils.py:939
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantcheckstatus.pt:57
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:179
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:185
+#: waeup/kofa/students/utils.py:953
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:192
+#: waeup/kofa/students/utils.py:1025
+msgid "void"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:198
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:208
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:322
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:413
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:418
+#: waeup/kofa/students/utils.py:421
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:438
+#: waeup/kofa/students/utils.py:477
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:494
+msgid "No bed allocated."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:501
+msgid "%s undefined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:513
+msgid "This category of payments has been disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:540
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:569
+msgid "Matriculation number cannot be set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:588
+msgid "Matriculation number already set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:590
+msgid "No certificate assigned."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:597
+msgid "Matriculation number %s exists."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:647
+#. Default: ""
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:650
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:654
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:663
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:679
+#. Default: ""
+msgid "This is to inform you that you have been provisionally admitted into ${a} as follows:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:727
+#. Default: ""
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:777
+#: waeup/kofa/students/viewlets.py:66
+#: waeup/kofa/students/viewlets.py:100
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:798
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:933
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:960
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:967
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114
+#: waeup/kofa/students/viewlets.py:927
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:141
+#: waeup/kofa/students/viewlets.py:932
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:147
+msgid "Final Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:204
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:214
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:243
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:255
+#: waeup/kofa/students/viewlets.py:276
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:264
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:294
+#: waeup/kofa/students/workflow.py:96
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:335
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:398
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:429
+#: waeup/kofa/students/workflow.py:180
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:449
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:469
+#: waeup/kofa/students/workflow.py:173
+msgid "Unregister courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:475
+msgid "'You really want to unregister your course list?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:495
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:553
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:563
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:592
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:641
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:656
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:676
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:695
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:705
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:722
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:738
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:824
+msgid "Sign transcript electronically"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:83
+#: waeup/kofa/students/viewlets.py:921
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:836
+msgid "'Signing a transcript electronically cannot be revoked. The electronic signature replaces your handwritten signature.\\n\\nYou really want to sign the transcript?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:931
+msgid "Accommodation Data"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:105
+#. Default: ""
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:110
+msgid "Level Zero"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:112
+#: waeup/kofa/students/vocabularies.py:115
+#. Default: ""
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:126
+#. Default: ""
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:129
+#. Default: ""
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:132
+#. Default: ""
+msgid "${title} 1st spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:134
+#. Default: ""
+msgid "${title} on 1st probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:136
+#. Default: ""
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:155
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:157
+msgid "female"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:110
+#: waeup/kofa/students/workflow.py:131
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:111
+msgid "First school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:118
+msgid "First school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:124
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:125
+msgid "Reset to 'cleared'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:132
+msgid "School fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:138
+msgid "Pay PG school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:139
+msgid "PG school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:145
+msgid "Approve school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:146
+msgid "School fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:152
+msgid "Approve PG school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:153
+msgid "PG school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:159
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:160
+msgid "Reset to 'returning'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:166
+msgid "Register courses"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:174
+msgid "Courses unregistered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:187
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:188
+msgid "Course validation bypassed"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:194
+msgid "Reset to school fee paid"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:195
+msgid "Reset to 'school fee paid'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:201
+msgid "Return"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:202
+msgid "Returned"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:208
+msgid "Reset to courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:209
+msgid "Reset to 'courses validated'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:217
+msgid "Request transript"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:218
+msgid "Transcript requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:224
+msgid "Reject transcript request"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:225
+msgid "Transcript request rejected"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:232
+msgid "Transcript validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:239
+msgid "Transcript released"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:245
+msgid "Reset to graduated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:246
+msgid "Transcript process reset"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Record created"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:54
+msgid "Admit student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:61
+msgid "Reset student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:62
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:69
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:75
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:76
+msgid "Reset to 'admitted'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:83
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:89
+#: waeup/kofa/students/workflow.py:103
+msgid "Reset to clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:90
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to 'clearance started'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:97
+msgid "Cleared"
+msgstr ""
+
+#: waeup/kofa/university/batching.py:187
+msgid "Department Processor"
+msgstr ""
+
+#: waeup/kofa/university/batching.py:240
+msgid "Certificate Processor"
+msgstr ""
+
+#: waeup/kofa/university/batching.py:324
+msgid "Course Processor"
+msgstr ""
+
+#: waeup/kofa/university/batching.py:379
+msgid "CertificateCourse Processor"
+msgstr ""
+
+#: waeup/kofa/university/batching.py:61
+msgid "Faculty Processor"
+msgstr ""
+
+#: waeup/kofa/university/export.py:122
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:175
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/kofa/university/export.py:209
+msgid "Courses in Certificates"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:104
+#: waeup/kofa/university/interfaces.py:212
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:105
+#: waeup/kofa/university/interfaces.py:213
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:112
+msgid "Department Officer 1"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:118
+msgid "Department Officer 2"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:124
+msgid "Department Officer 3"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:130
+msgid "Department Officer 4"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:159
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:184
+msgid "Former course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:185
+msgid "If this attribute is being set all certificate courses referring to this course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:193
+msgid "Results validated by"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:199
+msgid "Results validation date"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:205
+msgid "Results validation session"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:236
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:243
+msgid "Degree"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:249
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:256
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:263
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:270
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:276
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:282
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:288
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:294
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:301
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:306
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:311
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:316
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:323
+#: waeup/kofa/university/interfaces.py:325
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:327
+#: waeup/kofa/university/interfaces.py:330
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:366
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:48
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:54
+#: waeup/kofa/university/interfaces.py:97
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:61
+msgid "Faculty Officer 1"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:67
+msgid "Faculty Officer 2"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:91
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "None"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:41
+msgid "Postgraduate Level"
+msgstr ""
+
+#: waeup/kofa/userscontainer.py:72
+msgid "Users"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169
+#: waeup/kofa/utils/utils.py:53
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "R\303\251union"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barth\303\251lemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "C\303\264te d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Cura\303\247ao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "\303\205land Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90
+#: waeup/kofa/utils/utils.py:52
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:220
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:305
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:346
+msgid ""
+"Dear ${a},\n"
+"\n"
+"The candidate with Id ${b} and name ${c} applied to\n"
+"the ${d} to study ${e} for the ${f} session.\n"
+"The candidate has listed you as referee. You are thus required to kindly use\n"
+"the link below to provide your referral remarks on or before\n"
+"${g}.\n"
+"\n"
+"${h}\n"
+"\n"
+"Thank You\n"
+"\n"
+"The Secretary\n"
+"Post Graduate School\n"
+"${d}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:54
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/kofa/widgets/orderedSelectionList.pt:144
+#: waeup/kofa/widgets/orderedSelectionList.pt:165
+msgid "${DYNAMIC_CONTENT}"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:15
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:23
+#. Default: "Remove selected items"
+msgid "remove-selected-items"
+msgstr ""
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po	(revision 16045)
@@ -0,0 +1,6397 @@
+# #############################################################################
+#
+# Copyright (c) 2003-2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: WAeUP.Kofa\n"
+"POT-Creation-Date: Mon Jul  7 13:00:42 2014\n"
+"PO-Revision-Date: 2012-03-09 07:17+0100\n"
+"Last-Translator: Henrik Bettermann <henrik@waeup.org>\n"
+"Language-Team: WAeUP Nigeria <isouaba@waeup.org>\n"
+"Language: yo-NG\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+"X-Poedit-Language: Yoruba\n"
+"X-Poedit-Country: NIGERIA\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: waeup/kofa/accesscodes/browser.py:101 waeup/kofa/accesscodes/browser.py:114
+#: waeup/kofa/applicants/browser.py:238 waeup/kofa/applicants/browser.py:335
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/browser/pages.py:654 waeup/kofa/browser/pages.py:858
+#: waeup/kofa/browser/pages.py:884 waeup/kofa/browser/pages.py:1207
+#: waeup/kofa/browser/pages.py:1310 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1732 waeup/kofa/browser/pages.py:1764
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1817 waeup/kofa/browser/pages.py:1912
+#: waeup/kofa/browser/pages.py:1956 waeup/kofa/browser/pages.py:1957
+#: waeup/kofa/browser/pages.py:1959 waeup/kofa/browser/pages.py:2013
+#: waeup/kofa/browser/pages.py:2062 waeup/kofa/browser/pages.py:2103
+#: waeup/kofa/browser/pages.py:2134 waeup/kofa/browser/pages.py:2144
+#: waeup/kofa/browser/pages.py:2191 waeup/kofa/browser/pages.py:2193
+#: waeup/kofa/browser/pages.py:2215 waeup/kofa/browser/pages.py:2256
+#: waeup/kofa/browser/pages.py:2313 waeup/kofa/students/browser.py:952
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1757 waeup/kofa/students/browser.py:1795
+#: waeup/kofa/students/browser.py:1829
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:103
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:113
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:116
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:121
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:127
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:137
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:140
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:153
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154 waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1500 waeup/kofa/browser/viewlets.py:187
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Cancel Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:186
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:190
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:196
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:200
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:240
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:249
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:257
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:266
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:46 waeup/kofa/accesscodes/browser.py:211
+#: waeup/kofa/accesscodes/browser.py:231
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:69
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:78
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:84
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:92
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:94
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Cost"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:14
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:46
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:10
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:11
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:12
+msgid "with message term"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:29
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:30
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:6
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:38
+msgid "AccessCodeBatches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/export.py:69
+msgid "AccessCodes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:41
+#: waeup/kofa/accesscodes/interfaces.py:82
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:44
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:55
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:65
+msgid "Creation date"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:68
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:71
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:74
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:78
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:86
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/interfaces.py:91
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:37 waeup/kofa/applicants/workflow.py:39
+msgid "initialized"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:134
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:137
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:169
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/batching.py:258 waeup/kofa/students/batching.py:216
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1004 waeup/kofa/students/browser.py:107
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:102 waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1021
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1023
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:103
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1046
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1072
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1115
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1120
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1135 waeup/kofa/browser/pages.py:2362
+#: waeup/kofa/students/browser.py:2813
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158 waeup/kofa/students/browser.py:2830
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1164 waeup/kofa/applicants/browser.py:1177
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1168 waeup/kofa/students/browser.py:2839
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:117 waeup/kofa/browser/pages.py:1876
+#: waeup/kofa/students/browser.py:280 waeup/kofa/students/browser.py:321
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1188
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1205
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:122
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1222
+msgid "Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1235 waeup/kofa/browser/pages.py:1607
+#: waeup/kofa/students/browser.py:3110
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1258
+msgid "Export started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:130 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133 waeup/kofa/applicants/browser.py:197
+#: waeup/kofa/applicants/browser.py:335 waeup/kofa/applicants/browser.py:375
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:872
+#: waeup/kofa/applicants/browser.py:989 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1040
+#: waeup/kofa/browser/pages.py:625 waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:795 waeup/kofa/browser/pages.py:878
+#: waeup/kofa/browser/pages.py:1538 waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/pages.py:1813 waeup/kofa/browser/pages.py:1956
+#: waeup/kofa/browser/pages.py:1983 waeup/kofa/browser/pages.py:2134
+#: waeup/kofa/browser/pages.py:2140 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2201 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:165 waeup/kofa/hostels/browser.py:230
+#: waeup/kofa/hostels/browser.py:241 waeup/kofa/hostels/browser.py:336
+#: waeup/kofa/students/browser.py:538 waeup/kofa/students/browser.py:591
+#: waeup/kofa/students/browser.py:757 waeup/kofa/students/browser.py:859
+#: waeup/kofa/students/browser.py:952 waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1173 waeup/kofa/students/browser.py:1448
+#: waeup/kofa/students/browser.py:1486 waeup/kofa/students/browser.py:2274
+#: waeup/kofa/students/browser.py:2289 waeup/kofa/students/browser.py:2415
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:153
+#: waeup/kofa/applicants/browser.py:336 waeup/kofa/applicants/browser.py:394
+#: waeup/kofa/browser/pages.py:785 waeup/kofa/browser/pages.py:817
+#: waeup/kofa/browser/pages.py:900 waeup/kofa/browser/pages.py:1711
+#: waeup/kofa/browser/pages.py:1718 waeup/kofa/browser/pages.py:1794
+#: waeup/kofa/browser/pages.py:1804 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:138 waeup/kofa/students/browser.py:305
+#: waeup/kofa/students/browser.py:1939 waeup/kofa/students/browser.py:1946
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134 waeup/kofa/applicants/browser.py:178
+#: waeup/kofa/applicants/browser.py:209 waeup/kofa/applicants/browser.py:215
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:135 waeup/kofa/applicants/browser.py:187
+#: waeup/kofa/applicants/browser.py:338 waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/browser/pages.py:1795 waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1960 waeup/kofa/browser/pages.py:2022
+#: waeup/kofa/browser/pages.py:2135 waeup/kofa/browser/pages.py:2153
+#: waeup/kofa/browser/pages.py:2194 waeup/kofa/browser/pages.py:2224
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:136 waeup/kofa/applicants/browser.py:183
+#: waeup/kofa/applicants/browser.py:339 waeup/kofa/applicants/browser.py:448
+#: waeup/kofa/browser/pages.py:1796 waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1961 waeup/kofa/browser/pages.py:2018
+#: waeup/kofa/browser/pages.py:2136 waeup/kofa/browser/pages.py:2149
+#: waeup/kofa/browser/pages.py:2195 waeup/kofa/browser/pages.py:2220
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:137
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:159
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:170 waeup/kofa/applicants/browser.py:411
+#: waeup/kofa/applicants/browser.py:927
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:173 waeup/kofa/applicants/browser.py:414
+#: waeup/kofa/applicants/browser.py:930 waeup/kofa/browser/pages.py:155
+#: waeup/kofa/students/browser.py:349 waeup/kofa/students/browser.py:1036
+#: waeup/kofa/students/browser.py:1507 waeup/kofa/students/browser.py:1698
+#: waeup/kofa/students/browser.py:1967 waeup/kofa/students/browser.py:2662
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:201 waeup/kofa/applicants/browser.py:389
+#: waeup/kofa/applicants/browser.py:900 waeup/kofa/applicants/browser.py:1050
+#: waeup/kofa/browser/pages.py:197 waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/hostels/browser.py:54 waeup/kofa/students/browser.py:99
+#: waeup/kofa/students/browser.py:560
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:224
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:234
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:246 waeup/kofa/applicants/export.py:75
+#: waeup/kofa/applicants/viewlets.py:45
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:12
+msgid "Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:337 waeup/kofa/applicants/browser.py:419
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:345
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:400 waeup/kofa/applicants/browser.py:425
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:436 waeup/kofa/applicants/browser.py:613
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:439 waeup/kofa/applicants/browser.py:616
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:466 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:469
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:474
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:486
+msgid "Applicant"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:518
+msgid ""
+"\n"
+" <strong>Congratulations!</strong> You have been offered provisional "
+"admission into the ${c} Academic Session of ${d}. Your student record has "
+"been created for you. Please, logout again and proceed to the login page of "
+"the portal. Then enter your new student credentials: user name= ${a}, "
+"password = ${b}. Change your password when you have logged in."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:540 waeup/kofa/students/browser.py:441
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:442
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:546
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:648 waeup/kofa/students/utils.py:476
+#: waeup/kofa/students/utils.py:506
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:66
+msgid ""
+"You can not edit your application records after final submission. You really "
+"want to submit?"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:662 waeup/kofa/students/browser.py:1752
+#: waeup/kofa/students/browser.py:1791 waeup/kofa/students/browser.py:1824
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:685 waeup/kofa/students/browser.py:1848
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:730 waeup/kofa/students/browser.py:1905
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:736 waeup/kofa/students/browser.py:1911
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:742
+msgid ""
+"Please submit the application form before trying to download payment slips."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:75
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:763
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:785
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:807 waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:995 waeup/kofa/applicants/browser.py:1053
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:939
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:1029
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:808 waeup/kofa/applicants/browser.py:944
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/applicants/browser.py:993
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/applicants/browser.py:1034
+#: waeup/kofa/students/browser.py:1450 waeup/kofa/students/browser.py:1491
+#: waeup/kofa/students/browser.py:1679 waeup/kofa/students/browser.py:2670
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:857
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/students/browser.py:588
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:914 waeup/kofa/students/browser.py:1685
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:2
+msgid "Container Statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:23
+msgid "Number of Submissions"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:31
+msgid "Application State"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:7
+msgid "Number of Applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:23
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:133
+msgid ""
+"<strong>Notice:</strong> If you need to pay for another service, select the "
+"corresponding special application category above, click 'Save' and then 'Add "
+"online payment ticket' again."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:40
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:58
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:74
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:5
+msgid "(account suspended)"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Payment Tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:18
+msgid ""
+"An account will be created for you and an email with your login credentials "
+"will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:22
+msgid ""
+"Your application record will be looked up and an email with your login "
+"credentials will be sent to the address provided."
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "First Choice"
+msgstr ""
+
+#. Default: "There are more than 5000 application records in this container. In order to prevent from downloading big amounts of data, the Applicants tab has been disabled. Please use the ${search_link} for accessing application records."
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:94
+msgid "search form"
+msgstr ""
+
+#. Default: "Please note that containers will be opened in manage mode here. All application record links will be loaded into your browser which can take a considerable time depending on your bandwidth. Please be patient until all records have been loaded."
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:37
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:13
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:12
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:145
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:175
+#: waeup/kofa/applicants/interfaces.py:239
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:193 waeup/kofa/students/browser.py:1251
+#: waeup/kofa/students/browser.py:1402 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:68
+#: waeup/kofa/university/interfaces.py:111
+#: waeup/kofa/university/interfaces.py:155
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:24
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:69
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/browser/templates/searchpage.pt:18
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:199 waeup/kofa/students/browser.py:1252
+#: waeup/kofa/students/browser.py:1403 waeup/kofa/students/interfaces.py:629
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/searchpage.pt:19
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:42
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:205
+#: waeup/kofa/applicants/interfaces.py:319
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:212
+#: waeup/kofa/applicants/interfaces.py:326
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:219
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:233
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:252
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:254
+#: waeup/kofa/applicants/interfaces.py:260 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:258
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:264
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:270 waeup/kofa/interfaces.py:694
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/applicants/interfaces.py:487
+#: waeup/kofa/students/interfaces.py:205
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/interfaces.py:499
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:245 waeup/kofa/students/interfaces.py:387
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:505
+#: waeup/kofa/students/interfaces.py:224 waeup/kofa/students/interfaces.py:412
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/applicants/interfaces.py:510
+#: waeup/kofa/students/interfaces.py:229
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/applicants/interfaces.py:515
+#: waeup/kofa/applicants/interfaces.py:622
+#: waeup/kofa/students/interfaces.py:234
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/applicants/interfaces.py:528
+#: waeup/kofa/students/interfaces.py:317
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:239
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:535
+#: waeup/kofa/applicants/interfaces.py:565
+#: waeup/kofa/applicants/interfaces.py:627
+#: waeup/kofa/browser/interfaces.py:186 waeup/kofa/interfaces.py:466
+#: waeup/kofa/interfaces.py:542 waeup/kofa/students/interfaces.py:417
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/applicants/interfaces.py:541 waeup/kofa/interfaces.py:471
+#: waeup/kofa/interfaces.py:549 waeup/kofa/students/interfaces.py:270
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:570
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:575
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:586
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:219
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/browser/templates/findstudentspage.pt:30
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:580
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:440
+#: waeup/kofa/applicants/interfaces.py:493
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:547
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:520
+msgid "Identification Number"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:55 waeup/kofa/applicants/workflow.py:74
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:70 waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:78 waeup/kofa/students/payments.py:131
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/pdf.py:99
+msgid ""
+"\n"
+" Proceed to the login page of the portal and enter your new credentials: "
+"user name= ${a}, password = ${b}. Change your password when you have logged "
+"in."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:53 waeup/kofa/students/browser.py:142
+#: waeup/kofa/students/utils.py:404 waeup/kofa/students/utils.py:431
+#: waeup/kofa/students/utils.py:442 waeup/kofa/students/utils.py:502
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:71 waeup/kofa/students/utils.py:474
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:107
+msgid "Register for application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:124
+msgid "Container statistics"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:137
+msgid "Manage container"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:147
+msgid "Export applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:157 waeup/kofa/applicants/viewlets.py:166
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:174
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:182
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:190
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:209
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:226 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:364
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:243 waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:258 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:503 waeup/kofa/students/workflow.py:97
+msgid "Approve payment"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:58
+msgid "Application"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:74 waeup/kofa/students/viewlets.py:774
+msgid "My Data"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:109 waeup/kofa/students/batching.py:153
+msgid "Student record created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:115 waeup/kofa/applicants/workflow.py:122
+#: waeup/kofa/applicants/workflow.py:129 waeup/kofa/applicants/workflow.py:136
+#: waeup/kofa/applicants/workflow.py:150
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:116 waeup/kofa/applicants/workflow.py:123
+#: waeup/kofa/applicants/workflow.py:130 waeup/kofa/applicants/workflow.py:137
+#: waeup/kofa/applicants/workflow.py:151
+msgid "Application reset"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:143
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:144
+msgid "Application reset to paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:157
+msgid "Reset application to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:158
+msgid "Application reset to admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:40
+msgid "started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:41
+msgid "paid"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:42
+msgid "submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:43 waeup/kofa/interfaces.py:95
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:94
+msgid "created"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:51
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:54
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:59
+msgid "Start application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:60
+msgid "Application started"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:66
+msgid "Pay application fee"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:67
+msgid "Payment made"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:80
+msgid "Submit application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:81
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:87
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:88
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:94 waeup/kofa/applicants/workflow.py:101
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:95 waeup/kofa/applicants/workflow.py:102
+msgid "Application refused"
+msgstr ""
+
+#: waeup/kofa/browser/async.py:91
+msgid "Please wait..."
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:108
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:120 waeup/kofa/browser/viewlets.py:668
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:479
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:322
+#: waeup/kofa/browser/viewlets.py:719
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:526
+#: waeup/kofa/browser/viewlets.py:331 waeup/kofa/browser/viewlets.py:729
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:897
+#: waeup/kofa/browser/pages.py:1498 waeup/kofa/browser/viewlets.py:340
+#: waeup/kofa/browser/viewlets.py:738
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:349 waeup/kofa/browser/viewlets.py:747
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:3097
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:177
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:178
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:103 waeup/kofa/students/browser.py:742
+msgid "Are you sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:100
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1001
+msgid ""
+"File: ${a}\n"
+"Importer: ${b}\n"
+"Import Mode: ${c}\n"
+"Datasets: ${d}\n"
+"\n"
+"${e}\n"
+"\n"
+"Comment by Import Manager:"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1020
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1022 waeup/kofa/browser/pages.py:2394
+#: waeup/kofa/students/browser.py:494
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1030
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1045
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1053
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1064
+msgid ""
+"Your file contains forbidden characters or has invalid CSV format. First "
+"problematic line detected: line %s. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1176 waeup/kofa/browser/pages.py:1205
+#: waeup/kofa/browser/pages.py:1308 waeup/kofa/browser/pages.py:1420
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1208
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1209
+msgid "Proceed to step 3"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:121
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1252
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1260
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1273 waeup/kofa/browser/pages.py:1396
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1285
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1311 waeup/kofa/browser/pages.py:1539
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1312
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1313
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1314
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:134 waeup/kofa/browser/pages.py:906
+#: waeup/kofa/hostels/browser.py:266 waeup/kofa/hostels/browser.py:296
+#: waeup/kofa/students/browser.py:336
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1379
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1422
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1472
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1474
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1482
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1485
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1502
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1510
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1525
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1528
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1536
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1556
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1559
+msgid "Given storage path cannot be used. ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1563
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1575
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1577
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1699
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1711 waeup/kofa/browser/pages.py:1727
+#: waeup/kofa/browser/pages.py:1744 waeup/kofa/browser/pages.py:1748
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1716 waeup/kofa/browser/viewlets.py:406
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1721
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1755
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1781 waeup/kofa/browser/pages.py:1792
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1794 waeup/kofa/browser/pages.py:1822
+#: waeup/kofa/browser/pages.py:1890 waeup/kofa/browser/pages.py:1894
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1802 waeup/kofa/browser/viewlets.py:434
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1807
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1842 waeup/kofa/students/browser.py:260
+#: waeup/kofa/students/browser.py:304
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1847
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1881 waeup/kofa/students/browser.py:292
+#: waeup/kofa/students/browser.py:334 waeup/kofa/students/browser.py:2746
+#: waeup/kofa/students/browser.py:2958 waeup/kofa/students/browser.py:3054
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1901
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1905
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1927
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:1987
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1957 waeup/kofa/browser/pages.py:2003
+#: waeup/kofa/browser/pages.py:2032 waeup/kofa/browser/pages.py:2036
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:1993
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1958 waeup/kofa/browser/pages.py:2008
+#: waeup/kofa/browser/pages.py:2073 waeup/kofa/browser/pages.py:2077
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1967 waeup/kofa/browser/viewlets.py:451
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1996
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2048
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2054
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2089
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2095
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2132 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2185
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2192 waeup/kofa/browser/pages.py:2210
+#: waeup/kofa/browser/pages.py:2237 waeup/kofa/browser/pages.py:2239
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2193 waeup/kofa/browser/pages.py:2205
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2199 waeup/kofa/browser/viewlets.py:527
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2244
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2248
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2289 waeup/kofa/browser/viewlets.py:559
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2292
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2325
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2373
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2387 waeup/kofa/students/browser.py:2862
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2391
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:263 waeup/kofa/browser/viewlets.py:268
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:39
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:274
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:283
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:316
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:327
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:342
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:384
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:409 waeup/kofa/browser/pages.py:442
+#: waeup/kofa/browser/pages.py:687 waeup/kofa/students/browser.py:492
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:429
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:444
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:469
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:527 waeup/kofa/hostels/browser.py:217
+#: waeup/kofa/students/viewlets.py:176 waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:345
+#: waeup/kofa/students/viewlets.py:404 waeup/kofa/students/viewlets.py:471
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:528 waeup/kofa/browser/viewlets.py:177
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:535
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:572 waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:596
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:617
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:668 waeup/kofa/students/browser.py:473
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:697 waeup/kofa/browser/viewlets.py:356
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:710 waeup/kofa/browser/viewlets.py:372
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:767
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:781
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:782 waeup/kofa/browser/pages.py:822
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:784 waeup/kofa/browser/pages.py:801
+#: waeup/kofa/browser/pages.py:838
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:813
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:823
+msgid "For experts only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:824
+msgid ""
+"Plugins may only be updated after software upgrades. Are you really sure?"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:829
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:842
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:853
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:875
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:917
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:921
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:932
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:934 waeup/kofa/browser/pages.py:950
+#: waeup/kofa/browser/pages.py:1178 waeup/kofa/browser/pages.py:1484
+#: waeup/kofa/browser/pages.py:1501 waeup/kofa/browser/pages.py:1540
+#: waeup/kofa/browser/pages.py:1578
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:946
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:949
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:995
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:997
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:156
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:207 waeup/kofa/browser/pdf.py:472
+#: waeup/kofa/browser/pdf.py:482
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:533
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:803
+msgid "Date: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/reports.py:112
+msgid "Create report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:14
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:30
+msgid "Required course:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:9
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:40
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:41
+msgid "Dep. Course"
+msgstr ""
+
+#. Default: "Enter the email address stored in Kofa. You can't change your email address here. If you've forgotten your email address or the email address stored in Kofa is wrong or no longer valid, use the enquiries form to request a change of address."
+#: waeup/kofa/browser/templates/changepw.pt:19
+msgid "email_address_problem"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/kofa/browser/templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:9
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/kofa/browser/templates/contactform.pt:22
+msgid "Please solve the captcha to prevent misuse of this service."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:1
+msgid ""
+"Here you can create CSV files from parts of portal data except student data. "
+"Please pick the type of objects you want to export from the selection below. "
+"The file will be generated and then be made available for you to download in "
+"the table below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:37
+msgid ""
+"Student data exports have to be further configured. You can create CSV files "
+"on the <a href=\"@@exportconfig\">student data export configuration page</a>."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:3
+msgid ""
+"Find below the last ${count} imports. The files contain the imported "
+"datasets."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:18
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:2
+msgid ""
+"Using batch processing you can mass-create, mass-update, or mass-remove "
+"datasets from the database using CSV files."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:14
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:36
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:16
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:39
+msgid "Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "Sample Record"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:24
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:5
+msgid "Storage path"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:1
+msgid ""
+"The data center helps you to manage portal data. You can upload CSV files "
+"here, which will be available for import afterwards."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:1
+msgid ""
+"Before uploading a file check that your file header corresponds with header "
+"format of the selected processor. All available headers are listed below. "
+"After file upload click the 'Process data' button and proceed up to import "
+"step 3. Verify that the data format meets all the import criteria and "
+"requirements of the processor."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:30
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:43
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:62
+msgid ""
+"Import managers will be automatically informed by email after file upload. "
+"There is no need for assigning tickets or sending emails anymore. The data "
+"will be imported according to the information given."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:70
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:77
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:78
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:84
+msgid "Download CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:102
+#: waeup/kofa/browser/templates/coursemanagepage.pt:39
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:113
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:15
+#: waeup/kofa/browser/templates/coursemanagepage.pt:9
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:13
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:10
+msgid "Password"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#. Default: "Don't forget to logout or exit your browser when you're done. If you are having trouble logging in, make sure to enable cookies in your web browser."
+#: waeup/kofa/browser/templates/loginpage.pt:20
+msgid "login_trouble1"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Acquire a Password Activation Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:23
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "You don't have an account because you are a fresh student, or your student record has just been created? Inititialize your student account <strong><a href=\"requestpw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:28
+msgid "login_trouble4"
+msgstr ""
+
+#. Default: "Or simply forgot your student id, application id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/kofa/browser/templates/loginpage.pt:32
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:4
+msgid "User Name or Id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainercreate.pt:2
+msgid ""
+"Here you can create reports representing portal data. Please pick the type "
+"of report you want to create from the selection below. Afterwards you can "
+"configure the report (do the settings). The report data will be calculated "
+"and then made available for you to view/download."
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:50
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:46
+#: waeup/kofa/browser/templates/studentsitelayout.pt:45
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:52
+#: waeup/kofa/browser/templates/studentsitelayout.pt:51
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:6
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:167
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:307
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:389 waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:398
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:416 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:506 waeup/kofa/browser/viewlets.py:550
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:425
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:442 waeup/kofa/students/browser.py:259
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:462 waeup/kofa/browser/viewlets.py:494
+#: waeup/kofa/browser/viewlets.py:538
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:518
+msgid "Update scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:583
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:592
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:601
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:610
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:683
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:157
+#: waeup/kofa/students/viewlets.py:337 waeup/kofa/students/viewlets.py:544
+#: waeup/kofa/students/viewlets.py:592
+msgid "Edit"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:106
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:119 waeup/kofa/hostels/browser.py:129
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:132 waeup/kofa/hostels/browser.py:152
+#: waeup/kofa/hostels/browser.py:179
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:157
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:160
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:168
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:182
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:191
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:228
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:231 waeup/kofa/hostels/browser.py:246
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:232 waeup/kofa/hostels/browser.py:260
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:233 waeup/kofa/hostels/browser.py:290
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:234 waeup/kofa/hostels/browser.py:317
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:251
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:284
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:313
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:320
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:333
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:339
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:69 waeup/kofa/hostels/browser.py:79
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:96
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:6
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:50
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:43
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:100
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:34
+#: waeup/kofa/browser/templates/coursemanagepage.pt:37
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:111
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:51
+#: waeup/kofa/browser/templates/searchpage.pt:22
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:52
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:53
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:32
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:7
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:71
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:24
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:12
+msgid "Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:237
+msgid "unreserved"
+msgstr ""
+
+#: waeup/kofa/hostels/hostel.py:244
+msgid "reserved"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:104
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:110
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:116
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:127
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:134
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:141
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:148
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:155
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:162
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:169
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:176
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:183
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:193
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:203
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:225
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:231
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:242
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:243
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:258
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:265
+msgid "This student resides in bed ${a}."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:37
+msgid "Hostel Allocation Start Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:43
+msgid "Hostel Allocation Closing Date"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:49
+msgid "Booking Session"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:57
+msgid "Allowed States"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:88
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:92
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:98
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:38
+msgid "Bed A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:39
+msgid "Bed B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:40
+msgid "Bed C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:41
+msgid "Bed D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:42
+msgid "Bed E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:43
+msgid "Bed F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:44
+msgid "Bed G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:45
+msgid "Bed H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:46
+msgid "Bed I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:47
+msgid "Bed J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:48
+msgid "Bed K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:49
+msgid "Bed L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:53
+msgid "Block A"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:54
+msgid "Block B"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:55
+msgid "Block C"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:56
+msgid "Block D"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:57
+msgid "Block E"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:58
+msgid "Block F"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:59
+msgid "Block G"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:60
+msgid "Block H"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:61
+msgid "Block I"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:62
+msgid "Block J"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:75
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:100
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:104
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:238
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:242 waeup/kofa/students/browser.py:1255
+#: waeup/kofa/students/browser.py:1409
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:425
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:432
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:439
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:443
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:447
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:476 waeup/kofa/interfaces.py:532
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:486 waeup/kofa/interfaces.py:487
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:493
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:494
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:98
+#: waeup/kofa/browser/templates/coursemanagepage.pt:35
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:85
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:109
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:528
+#: waeup/kofa/students/browser_templates/containerpage.pt:42
+#: waeup/kofa/browser/templates/findstudentspage.pt:34
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:538
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:555
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:603
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:614
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:632
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:639
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:645
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:652
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:659
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:665
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:673
+msgid "Next Matriculation Number Integer"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:674
+msgid "Integer used for constructing the next matriculation number"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:68
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:686 waeup/kofa/interfaces.py:747
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:700
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:706
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:712
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:718
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:724
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:729
+msgid "Payment disabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:36
+msgid "Misuse"
+msgstr ""
+
+#: waeup/kofa/mandates/browser.py:41
+msgid "No mandate."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:50
+msgid "Nothing to do."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:68
+msgid "Wrong mandate parameters."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:70
+msgid "Mandate expired."
+msgstr ""
+
+#: waeup/kofa/mandates/mandate.py:72
+msgid "Password has been successfully set. Login with your new password."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:66
+msgid "by"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:106
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:113
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:120
+msgid "Response Code"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:127
+msgid "Response Description"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:26
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:27
+msgid "Paid"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:28
+msgid "Failed"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:50 waeup/kofa/students/interfaces.py:815
+#: waeup/kofa/students/interfaces.py:839
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:63
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:69 waeup/kofa/students/interfaces.py:822
+#: waeup/kofa/students/interfaces.py:846
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:75
+msgid "Payment State"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:82
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:88
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/kofa/students/browser_templates/exportconfig.pt:54
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:44
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:100
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/payments/payment.py:96
+msgid "Payment approved by ${a}"
+msgstr ""
+
+#: waeup/kofa/reports.py:175
+msgid "Unnamed Report"
+msgstr ""
+
+#: waeup/kofa/students/batching.py:854
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1004
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1014
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1016 waeup/kofa/students/browser.py:2592
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1026
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1050 waeup/kofa/students/viewlets.py:697
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1054
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1060 waeup/kofa/students/browser.py:2350
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1077 waeup/kofa/students/browser.py:2045
+#: waeup/kofa/students/browser.py:2370 waeup/kofa/students/browser.py:2517
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1080 waeup/kofa/students/browser.py:2085
+#: waeup/kofa/students/browser.py:2373 waeup/kofa/students/browser.py:2521
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1085 waeup/kofa/students/browser.py:2089
+#: waeup/kofa/students/browser.py:2378 waeup/kofa/students/browser.py:2525
+#: waeup/kofa/students/browser.py:2775
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1089 waeup/kofa/students/browser.py:2094
+#: waeup/kofa/students/browser.py:2382 waeup/kofa/students/browser.py:2530
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1112
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1124
+msgid "Process transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "Save comment and mark as processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1142
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1157 waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+msgid "Transcript processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1170 waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1189
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1205
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1223
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1239 waeup/kofa/students/viewlets.py:374
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1243
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1250 waeup/kofa/students/browser.py:1401
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:23
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1253 waeup/kofa/students/interfaces.py:655
+#: waeup/kofa/university/interfaces.py:123
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1254 waeup/kofa/students/browser.py:1408
+#: waeup/kofa/students/interfaces.py:666 waeup/kofa/students/interfaces.py:700
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:47
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283 waeup/kofa/students/viewlets.py:211
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1288
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1295
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1298
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1301
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1311
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1315
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1320
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1323
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1353
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1374
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1383
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1391
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1393
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1395
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1404
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1405
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/browser/templates/searchpage.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1406
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:28
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:45
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:144
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1449 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:1594 waeup/kofa/students/browser.py:2715
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1451
+msgid "Enter valid course code"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1460 waeup/kofa/students/browser.py:2620
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1466 waeup/kofa/students/browser.py:2625
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1483
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1497 waeup/kofa/students/browser.py:2649
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1527 waeup/kofa/students/browser.py:1553
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1532
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1534 waeup/kofa/students/browser.py:1566
+msgid "Student is in the wrong state."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:154
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1559
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1563
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:161
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1618
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:163
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1643
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1676
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1718
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1726 waeup/kofa/students/browser.py:1778
+#: waeup/kofa/students/browser.py:1809
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1736 waeup/kofa/students/browser.py:2026
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1742
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1768
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1773
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1806
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:184 waeup/kofa/students/export.py:232
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1943
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1957
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1989
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1991
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2008
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2011
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2015
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2019
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2032
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2057 waeup/kofa/students/browser.py:2073
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2079 waeup/kofa/students/browser.py:2218
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208 waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:210
+msgid "Previous Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2107 waeup/kofa/students/browser.py:2232
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2116
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2133
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2152
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:216 waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2161
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2189
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2240
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2259
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2271
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2286 waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2300
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:231 waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2312
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2328 waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2332
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2354
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2358
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2387
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2397
+msgid "Edit clearance data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:240
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2418 waeup/kofa/students/browser.py:2432
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2426
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2443 waeup/kofa/students/browser.py:2478
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2447 waeup/kofa/students/browser.py:2482
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2456 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2457
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2460
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2472
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2491
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2495
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2504
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2544
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2547
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2566
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2577 waeup/kofa/students/browser.py:2594
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2583
+msgid "Create course list now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2637
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2678
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2686
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2691
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2695
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2731
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2734
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2749
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2757
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2760
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2772
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2781
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2793
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836 waeup/kofa/students/browser.py:2848
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:286 waeup/kofa/students/browser.py:327
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2882
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2898 waeup/kofa/students/interfaces.py:448
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig.pt:23
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:23
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:55
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2899 waeup/kofa/students/interfaces.py:455
+#: waeup/kofa/students/interfaces.py:487 waeup/kofa/students/interfaces.py:545
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig.pt:33
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:33
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2949
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2955
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2991 waeup/kofa/students/interfaces.py:571
+#: waeup/kofa/students/interfaces.py:747 waeup/kofa/students/utils.py:255
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2992 waeup/kofa/students/utils.py:257
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:33
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:303 waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3043
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3047
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3083
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3123
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3131
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3138
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3145
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3192
+msgid "Payment dates do not match format d/m/Y."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3240
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3331
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:361 waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:369
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:383
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:405
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:409
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:415
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:433
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:435
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:512
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:530
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:572
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:612
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:631
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:666
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:696 waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:7
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:702
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:711
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:712
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:719
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:720
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:741
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:776
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:778 waeup/kofa/students/browser.py:813
+#: waeup/kofa/students/browser.py:1131
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:790 waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:802
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:806
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:810
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:843
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:856
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:871
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:874
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:901
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:904
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:932
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:934
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:939
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:950
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:953 waeup/kofa/students/browser.py:1020
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:954 waeup/kofa/students/browser.py:999
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:978
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:30
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:52
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:12
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:67
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:79
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/browser/templates/findstudentspage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/browser/templates/findstudentspage.pt:12
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:16
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:16
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:19
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:19
+msgid "who requested transcript (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:30
+#: waeup/kofa/browser/templates/findstudentspage.pt:22
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:39
+#: waeup/kofa/browser/templates/findstudentspage.pt:31
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:40
+#: waeup/kofa/browser/templates/findstudentspage.pt:32
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:41
+#: waeup/kofa/browser/templates/findstudentspage.pt:33
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:10
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:53
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:52
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+msgid "Course Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:51
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:52
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:36
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#. Default: "<strong>NOTICE:</strong> Student data exports are very computation intensive. Depending on the filter selected and the portal traffic load, exports may take longer than 15 minutes. You are advised to export data outside high-traffic time periods."
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:1
+msgid "student_export_warning"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:13
+msgid "Exporter (Data Type)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:43
+msgid "Current Study Mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:34
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:77
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:39
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:34
+msgid "<img src=\"/static/img/actionicon_reload.png\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:50
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:44
+msgid "and"
+msgstr ""
+
+#. Default: "Kofa is creating a temporary student password which will replace the original password chosen by the student. The password will be valid for ${minutes} minutes. The student can't login during this time period. After expiration the student can login with the original password again."
+#: waeup/kofa/students/browser_templates/loginasstep1.pt:6
+msgid "login_as_intro_1"
+msgstr ""
+
+#. Default: "The random password \"${password}\" has been set. You can proceed by clicking the button below."
+#: waeup/kofa/students/browser_templates/loginasstep2.pt:5
+msgid "login_as_intro_2"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:101
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Category"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:102
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Item"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Amount"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:98
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:51
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:99
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:18
+msgid ""
+"Your student record will be looked up and your login credentials will be "
+"sent to the email address given above. To be able to proceed you must "
+"provide a valid email address!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpw.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:14
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:44
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:14
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:44
+msgid "Thanks for using Kofa!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:20
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:20
+msgid "Your login credentials are:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:23
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:23
+msgid "User Name:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:46
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:3
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:3
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:39
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:32
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:32
+msgid "Print this page and proceed to the"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:34
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:34
+msgid ""
+"Please note that passwords are case-sensitive, <br />when entering your "
+"credentials, and keep your password secret!"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/reverttransfer.pt:4
+msgid ""
+"Student transfers can be reverted one after the other. When reverting a "
+"transfer, the active study course will be entirely removed including all "
+"containing course lists (study levels) and the previous study course will be "
+"reactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:34
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:35
+msgid ""
+"<li> Acquire a password access code (PWD). </li> <li> Enter your (JAMB) "
+"registration number and the access code. </li> <li> After pressing the "
+"button, your student id will be displayed and your password set. The 10-"
+"digit part of the access code just used is your new password. </li> <li> Go "
+"to the <strong><a href=\"login\">general login page</a></strong> and enter "
+"your new credentials. </li>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:12
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+msgid "Level Title"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:7
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:87
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:83
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:48
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:68
+msgid ""
+"You don't know the course code? Then select a course <a href=\"ctadd\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:7
+msgid "Course Tickets (Total Credits: ${total_credits})"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:7
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:94
+msgid ""
+"You don't know the course code? Then select a course <a href=\"add\">here</"
+"a>."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:46
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:33
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:49
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:55
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:20
+msgid "Cumulative GPA"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcript.pt:29
+msgid "Subjects/Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:22
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:6
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:10
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:11
+msgid "View correspondence"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:23
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:31
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:39
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:56
+#: waeup/kofa/students/browser_templates/enterpin.pt:22
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:6
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:255
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:285
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:311
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:18
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:12
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:345
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:382
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:449
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:482
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:511
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:211
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:213
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:252 waeup/kofa/students/interfaces.py:396
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:259
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:265
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:300
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:305
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:311
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:324
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:333
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:343
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:349 waeup/kofa/students/interfaces.py:378
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:359
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:427 waeup/kofa/students/interfaces.py:481
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:54
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:433 waeup/kofa/students/interfaces.py:513
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:440 waeup/kofa/students/interfaces.py:494
+#: waeup/kofa/students/interfaces.py:520
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:462 waeup/kofa/students/interfaces.py:533
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:469
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:551
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:556 waeup/kofa/students/interfaces.py:584
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:577
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:590
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:596
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:634
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:639
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:644
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:650
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:660
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:43
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:679
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:690
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:706
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:23
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:735
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:741
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:760
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:781
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:787 waeup/kofa/students/interfaces.py:828
+#: waeup/kofa/students/interfaces.py:852
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:858
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:862
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/payments.py:123
+msgid "Payment approved."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:2
+msgid ""
+"Please pick the department, session and level for which the report should be "
+"generated. The report will be processed and then made available for you to "
+"view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:27
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:39
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:26
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:9
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+#: waeup/kofa/browser/templates/searchpage.pt:21
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:2
+msgid ""
+"Please pick the session and study modes group for which the report should be "
+"generated. The report data will be calculated and then made available for "
+"you to view/download."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:9
+msgid "Study Modes Group"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:193
+#: waeup/kofa/students/reports/level_report.py:298
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:218
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:248
+#: waeup/kofa/students/reports/student_statistics.py:198
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+#: waeup/kofa/students/reports/session_results_presentation.py:238
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:206
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+#: waeup/kofa/students/reports/student_statistics.py:248
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:178
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:126
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:145
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:152 waeup/kofa/students/utils.py:833
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:161 waeup/kofa/students/utils.py:826
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:172
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:178 waeup/kofa/students/utils.py:840
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:187
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:197
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:292
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:386
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:391 waeup/kofa/students/utils.py:394
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:411 waeup/kofa/students/utils.py:450
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:470
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:478
+msgid "Payment temporarily disabled."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:504
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:521
+msgid "Matriculation number cannot be set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:543
+msgid "Matriculation number already set."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:545
+msgid "No certificate assigned."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:552
+msgid "Matriculation number exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:602
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:649
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:699 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:8
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:720
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:820
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:847
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:854
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:885
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:913
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:121 waeup/kofa/students/viewlets.py:797
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:144 waeup/kofa/students/viewlets.py:802
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:33
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:185
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:194
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:203
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:220
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232 waeup/kofa/students/viewlets.py:252
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:240
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:268 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:303
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:312 waeup/kofa/students/viewlets.py:321
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:359
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:390
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:419 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:437
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:456
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:518
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:527
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:536
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:562
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:607
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:621
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:640
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:658
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:667
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:683
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:801
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:813
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:839
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:844
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:850
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:860
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:878
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:879
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:902
+msgid "Upload selected file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:903
+msgid "Delete"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92 waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:940
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:954
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:12
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:965
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:980 waeup/kofa/students/viewlets.py:991
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:981 waeup/kofa/students/viewlets.py:992
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:112
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:115
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:118
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:120
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:122
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:93
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:104
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:105
+msgid "Reset to 'cleared'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:112
+msgid "School fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:118
+msgid "Pay PG school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:119
+msgid "PG school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:125
+msgid "Approve school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:126
+msgid "School fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:132
+msgid "Approve PG school fee payment"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:133
+msgid "PG school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:139
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:140
+msgid "Reset to 'returning'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:146
+msgid "Register courses"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:147
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:153 waeup/kofa/students/workflow.py:174
+msgid "Reset to school fee paid"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:154 waeup/kofa/students/workflow.py:175
+msgid "Reset to 'school fee paid'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:161
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:167
+msgid "Return and bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:168
+msgid "Course validation bypassed"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:181
+msgid "Return"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:182
+msgid "Returned"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:188
+msgid "Reset to courses validated"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:189
+msgid "Reset to 'courses validated'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:197
+msgid "Request transript"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:198
+msgid "Transcript requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:26
+msgid "Create student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:29
+msgid "Record created"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:34
+msgid "Admit student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:35
+msgid "Admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:41
+msgid "Reset student"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:42
+msgid "Reset to initial state"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:49
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:55
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:56
+msgid "Reset to 'admitted'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:63
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:69 waeup/kofa/students/workflow.py:83
+msgid "Reset to clearance started"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:70 waeup/kofa/students/workflow.py:84
+msgid "Reset to 'clearance started'"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:77
+msgid "Cleared"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:90 waeup/kofa/students/workflow.py:111
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:91
+msgid "First school fee payment made"
+msgstr ""
+
+#: waeup/kofa/students/workflow.py:98
+msgid "First school fee payment approved"
+msgstr ""
+
+#: waeup/kofa/university/export.py:114
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:9
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:12
+#: waeup/kofa/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/kofa/university/export.py:191
+msgid "Courses in Certificates"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:117
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:142
+msgid "Former course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:143
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:167
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:174
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:181
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:188
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:195
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:200
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:205
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:210
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:215
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:222
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:227
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:232
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:237
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:244
+#: waeup/kofa/university/interfaces.py:246
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:248
+#: waeup/kofa/university/interfaces.py:251
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:281
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:44
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:50
+#: waeup/kofa/university/interfaces.py:80
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:74
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:87
+msgid "Score editing disabled"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:88
+msgid "Lectures can not edit scores if ticked."
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:40
+msgid "Postgraduate Level"
+msgstr ""
+
+#: waeup/kofa/userscontainer.py:79
+msgid "Users"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:10
+msgid "Albania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:100
+msgid "Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:101
+msgid "Guinea-bissau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:102
+msgid "Guyana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:103
+msgid "Haiti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:104
+msgid "Heard Island and Mcdonald Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:105
+msgid "Holy See (Vatican City State)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:106
+msgid "Honduras"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:107
+msgid "Hong Kong"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:108
+msgid "Hungary"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:109
+msgid "Iceland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:11
+msgid "Algeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:110
+msgid "India"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:111
+msgid "Indonesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:112
+msgid "Iran, Islamic Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:113
+msgid "Iraq"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:114
+msgid "Ireland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:115
+msgid "Isle of Man"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:116
+msgid "Israel"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:117
+msgid "Italy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:118
+msgid "Jamaica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:119
+msgid "Japan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:12
+msgid "American Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:120
+msgid "Jersey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:121
+msgid "Jordan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:122
+msgid "Kazakhstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:123
+msgid "Kenya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:124
+msgid "Kiribati"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:125
+msgid "Korea, Democratic People's Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:126
+msgid "Korea, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:127
+msgid "Kuwait"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:128
+msgid "Kyrgyzstan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:129
+msgid "Lao People's Democratic Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:13
+msgid "Andorra"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:130
+msgid "Latvia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:131
+msgid "Lebanon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:132
+msgid "Lesotho"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:133
+msgid "Liberia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:134
+msgid "Libya"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:135
+msgid "Liechtenstein"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:136
+msgid "Lithuania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:137
+msgid "Luxembourg"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:138
+msgid "Macao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:139
+msgid "Macedonia, the Former Yugoslav Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:14
+msgid "Angola"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:140
+msgid "Madagascar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:141
+msgid "Malawi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:142
+msgid "Malaysia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:143
+msgid "Maldives"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:144
+msgid "Mali"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:145
+msgid "Malta"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:146
+msgid "Marshall Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:147
+msgid "Martinique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:148
+msgid "Mauritania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:149
+msgid "Mauritius"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:15
+msgid "Anguilla"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:150
+msgid "Mayotte"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:151
+msgid "Mexico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:152
+msgid "Micronesia, Federated States of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:153
+msgid "Moldova, Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:154
+msgid "Monaco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:155
+msgid "Mongolia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:156
+msgid "Montenegro"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:157
+msgid "Montserrat"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:158
+msgid "Morocco"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:159
+msgid "Mozambique"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:16
+msgid "Antarctica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:160
+msgid "Myanmar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:161
+msgid "Namibia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:162
+msgid "Nauru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:163
+msgid "Nepal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:164
+msgid "Netherlands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:165
+msgid "New Caledonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:166
+msgid "New Zealand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:167
+msgid "Nicaragua"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:168
+msgid "Niger"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:169 waeup/kofa/utils/utils.py:49
+msgid "Nigeria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:17
+msgid "Antigua and Barbuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:170
+msgid "Niue"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:171
+msgid "Norfolk Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:172
+msgid "Northern Mariana Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:173
+msgid "Norway"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:174
+msgid "Oman"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:175
+msgid "Pakistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:176
+msgid "Palau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:177
+msgid "Palestinian Territory, Occupied"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:178
+msgid "Panama"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:179
+msgid "Papua New Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:18
+msgid "Argentina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:180
+msgid "Paraguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:181
+msgid "Peru"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:182
+msgid "Philippines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:183
+msgid "Pitcairn"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:184
+msgid "Poland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:185
+msgid "Portugal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:186
+msgid "Puerto Rico"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:187
+msgid "Qatar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:188
+msgid "Réunion"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:189
+msgid "Romania"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:19
+msgid "Armenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:190
+msgid "Russian Federation"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:191
+msgid "Rwanda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:192
+msgid "Saint Barthélemy"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:193
+msgid "Saint Helena, Ascension and Tristan Da Cunha"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:194
+msgid "Saint Kitts and Nevis"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:195
+msgid "Saint Lucia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:196
+msgid "Saint Martin (French Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:197
+msgid "Saint Pierre and Miquelon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:198
+msgid "Saint Vincent and the Grenadines"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:199
+msgid "Samoa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:20
+msgid "Aruba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:200
+msgid "San Marino"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:201
+msgid "Sao Tome and Principe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:202
+msgid "Saudi Arabia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:203
+msgid "Senegal"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:204
+msgid "Serbia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:205
+msgid "Seychelles"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:206
+msgid "Sierra Leone"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:207
+msgid "Singapore"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:208
+msgid "Sint Maarten (Dutch Part)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:209
+msgid "Slovakia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:21
+msgid "Australia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:210
+msgid "Slovenia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:211
+msgid "Solomon Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:212
+msgid "Somalia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:213
+msgid "South Africa"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:214
+msgid "South Georgia and the South Sandwich Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:215
+msgid "South Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:216
+msgid "Spain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:217
+msgid "Sri Lanka"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:218
+msgid "Sudan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:219
+msgid "Suriname"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:22
+msgid "Austria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:220
+msgid "Svalbard and Jan Mayen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:221
+msgid "Swaziland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:222
+msgid "Sweden"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:223
+msgid "Switzerland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:224
+msgid "Syrian Arab Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:225
+msgid "Taiwan, Province of China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:226
+msgid "Tajikistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:227
+msgid "Tanzania, United Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:228
+msgid "Thailand"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:229
+msgid "Timor-leste"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:23
+msgid "Azerbaijan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:230
+msgid "Togo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:231
+msgid "Tokelau"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:232
+msgid "Tonga"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:233
+msgid "Trinidad and Tobago"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:234
+msgid "Tunisia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:235
+msgid "Turkey"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:236
+msgid "Turkmenistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:237
+msgid "Turks and Caicos Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:238
+msgid "Tuvalu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:239
+msgid "Uganda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:24
+msgid "Bahamas"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:240
+msgid "Ukraine"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:241
+msgid "United Arab Emirates"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:242
+msgid "United Kingdom"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:243
+msgid "United States"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:244
+msgid "United States Minor Outlying Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:245
+msgid "Uruguay"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:246
+msgid "Uzbekistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:247
+msgid "Vanuatu"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:248
+msgid "Venezuela, Bolivarian Republic of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:249
+msgid "Viet Nam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:25
+msgid "Bahrain"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:250
+msgid "Virgin Islands, British"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:251
+msgid "Virgin Islands, U.S."
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:252
+msgid "Wallis and Futuna"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:253
+msgid "Western Sahara"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:254
+msgid "Yemen"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:255
+msgid "Zambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:256
+msgid "Zimbabwe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:26
+msgid "Bangladesh"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:27
+msgid "Barbados"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:28
+msgid "Belarus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:29
+msgid "Belgium"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:30
+msgid "Belize"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:31
+msgid "Benin"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:32
+msgid "Bermuda"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:33
+msgid "Bhutan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:34
+msgid "Bolivia, Plurinational State of"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:35
+msgid "Bonaire, Sint Eustatius and Saba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:36
+msgid "Bosnia and Herzegovina"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:37
+msgid "Botswana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:38
+msgid "Bouvet Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:39
+msgid "Brazil"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:40
+msgid "British Indian Ocean Territory"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:41
+msgid "Brunei Darussalam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:42
+msgid "Bulgaria"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:43
+msgid "Burkina Faso"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:44
+msgid "Burundi"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:45
+msgid "Cambodia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:46
+msgid "Cameroon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:47
+msgid "Canada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:48
+msgid "Cape Verde"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:49
+msgid "Cayman Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:50
+msgid "Central African Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:51
+msgid "Chad"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:52
+msgid "Chile"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:53
+msgid "China"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:54
+msgid "Christmas Island"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:55
+msgid "Cocos (Keeling) Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:56
+msgid "Colombia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:57
+msgid "Comoros"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:58
+msgid "Congo"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:59
+msgid "Congo, the Democratic Republic of the"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:60
+msgid "Cook Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:61
+msgid "Costa Rica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:62
+msgid "Côte d'Ivoire"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:63
+msgid "Croatia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:64
+msgid "Cuba"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:65
+msgid "Curaçao"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:66
+msgid "Cyprus"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:67
+msgid "Czech Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:68
+msgid "Denmark"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:69
+msgid "Djibouti"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:70
+msgid "Dominica"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:71
+msgid "Dominican Republic"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:72
+msgid "Ecuador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:73
+msgid "Egypt"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:74
+msgid "El Salvador"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:75
+msgid "Equatorial Guinea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:76
+msgid "Eritrea"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:77
+msgid "Estonia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:78
+msgid "Ethiopia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:79
+msgid "Falkland Islands (Malvinas)"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:8
+msgid "Afghanistan"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:80
+msgid "Faroe Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:81
+msgid "Fiji"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:82
+msgid "Finland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:83
+msgid "France"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:84
+msgid "French Guiana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:85
+msgid "French Polynesia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:86
+msgid "French Southern Territories"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:87
+msgid "Gabon"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:88
+msgid "Gambia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:89
+msgid "Georgia"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:9
+msgid "Åland Islands"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:90 waeup/kofa/utils/utils.py:48
+msgid "Germany"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:91
+msgid "Ghana"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:92
+msgid "Gibraltar"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:93
+msgid "Greece"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:94
+msgid "Greenland"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:95
+msgid "Grenada"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:96
+msgid "Guadeloupe"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:97
+msgid "Guam"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:98
+msgid "Guatemala"
+msgstr ""
+
+#: waeup/kofa/utils/countries.py:99
+msgid "Guernsey"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:196
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:249
+msgid ""
+"Dear ${a},\n"
+"\n"
+"${b}\n"
+"Student Registration and Information Portal of\n"
+"${c}.\n"
+"\n"
+"Your user name: ${d}\n"
+"Your password: ${e}\n"
+"${f}\n"
+"\n"
+"Please remember your user name and keep\n"
+"your password secret!\n"
+"\n"
+"Please also note that passwords are case-sensitive.\n"
+"\n"
+"Regards\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:50
+msgid "U.S."
+msgstr ""
+
+#: waeup/kofa/widgets/datetimewidget.py:82
+msgid "Invalid datetime data"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:122
+msgid "Empty phone field(s)."
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:126
+msgid "Int. prefix requires format '+NNN'"
+msgstr ""
+
+#: waeup/kofa/widgets/phonewidget.py:134
+msgid "Phone numbers may contain numbers only."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:17
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:25
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/zope.pot
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/zope.pot	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/locales/zope.pot	(revision 16045)
@@ -0,0 +1,56 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Development/Unknown\n"
+"POT-Creation-Date: Fri Mar 23 16:41:56 2012\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope3-dev@zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Default: "Nothing"
+#: src/zope/app/form/browser/source.py:65
+#: src/zope/app/form/browser/source.py:212
+#: src/zope/app/form/browser/source.py:222
+msgid "SourceDisplayWidget-missing"
+msgstr ""
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:231
+msgid "item-missing-single-value-for-display"
+msgstr ""
+
+#. Default: ""
+#: src/zope/app/form/browser/itemswidgets.py:250
+msgid "vocabulary-missing-multiple-value-for-display"
+msgstr ""
+
+#. Default: "(no value)"
+#: src/zope/app/form/browser/itemswidgets.py:417
+#: src/zope/app/form/browser/itemswidgets.py:447
+msgid "vocabulary-missing-single-value-for-edit"
+msgstr ""
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/itemswidgets.py:486
+msgid "vocabulary-missing-multiple-value-for-edit"
+msgstr ""
+
+#. Default: "(no value available)"
+#: src/zope/app/form/browser/sequencewidget.py:256
+msgid "sequence-value-not-provided"
+msgstr ""
+
+#. Default: "(no values)"
+#: src/zope/app/form/browser/sequencewidget.py:259
+msgid "sequence-value-is-empty"
+msgstr ""
+
+#: src/zope/app/form/browser/boolwidgets.py:121
+msgid "True"
+msgstr ""
+
+#: src/zope/app/form/browser/boolwidgets.py:122
+msgid "False"
+msgstr ""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mail.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mail.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mail.zcml	(revision 16045)
@@ -0,0 +1,82 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:mail="http://namespaces.zope.org/mail">
+  <!--
+
+   SMTP Settings
+   *************
+
+   In here we define the mailers and mail deliveries we want to be
+   used by the portal.
+
+   They are set here (and not dynamically in the portal) to increase
+   security. Even portal users with all permissions cannot change mail
+   server credentials and related. You need filesystem access to
+   change/override the settings in here.
+
+   In the portal you can pick one of the deliveries for use with all
+   SMTP (aka mail) related functionality.
+
+   Each delivery (direct or queued) is bound to a certain mailer,
+   defined by a 'mail:smtpMailer' directive.
+
+   The smtpMailer directives define real mail hosts (local or remote)
+   where mail from the portal should be processed.
+
+   The 'mail:directDelivery' and 'mail:queuedDelivery' directives send
+   local mail to the mailer set in their 'mailer' attribute. While
+   directDelivery sends mail immediately when a request is finished
+   (see zope.sendmail for more info), queuedDelivery handles mail in a
+   background thread and sends mail independently from requests.
+
+   By default in KOFA we have a fake mailer activated, that does not
+   send real mail to the outside world.
+  -->
+
+  <!-- A mail server on localhost -->
+  <!-- Mail server definitions require the following attributes:
+
+        - name: (string) name to be used by deliveries
+
+       Mail server definitions allow the following additional attribs:
+
+        - hostname   (string) hostname or IP(?) of mail server to use
+                              ('localhost' by default)
+        - port       (int number) port to connect to SMTP mailer
+                              (25 by default)
+        - username   (string) Username to log in to mail server
+        - password   (string) Password to log in to mail server
+
+  -->
+  <mail:smtpMailer
+      name="smtp.localhost"
+      hostname="localhost"
+      port="25"
+      />
+
+  <!-- The mail deliveries -->
+  <!-- Mail deliveries require the following attributes:
+
+        - name:       (string) this string will appear in portal select
+                               boxes
+        - permission: (string) zope default perms only
+        - mailer:     (string) name of some mailer registered above or
+                               'smtp'
+        - queuePath:  (string) must exist and contain cur/, new/, and
+                               tmp/ dirs. Relative paths are relative
+                               to the directory this files lives in.
+
+   -->
+  <mail:queuedDelivery
+      name="Queued SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      queuePath="../../../var/mailqueue"
+      />
+
+  <mail:directDelivery
+      name="Direct SMTP on localhost"
+      permission="zope.Public"
+      mailer="smtp.localhost"
+      />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/maintenance.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/maintenance.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/maintenance.py	(revision 16045)
@@ -0,0 +1,155 @@
+"""Helpers for maintainers of kofa sites.
+
+XXX: This stuff might go into a separate package, but right
+     now it is too less for a complete package.
+"""
+import sys
+from ZODB.FileStorage import FileStorage
+from ZODB.scripts.analyze import report, analyze, shorten
+from zope.component.hooks import setSite
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.intid.interfaces import IIntIds
+
+
+def db_analyze(args=None):
+    """Run the analyze tool from ZODB package.
+    """
+    if args is None:
+        args = sys.argv[1:]
+
+    path = None
+    if len(args) > 0:
+        path = args[0]
+    else:
+        print
+        print "Analyze a ZODB file and print statistics"
+        print "about contained objects, sizes, etc."
+        print
+        print "Usage: %s <path-to-Data.fs>" % sys.argv[0]
+        print
+        sys.exit(0)
+
+    report(analyze(path))
+
+def update_catalog(site, cat_name, objects=[], func=None):
+    """Update a catalog.
+
+    Put `objects` or objects delivered by `func()` into the catalog
+    registered under `cat_name` in `site`.
+
+    Objects to be catalogued must be 'located', i.e. they must have a
+    __name__ and __parent__ (because they are adapted to
+    IKeyReference).
+
+    You can pass in objects as some iterable or as a function that is
+    called to deliver the set of objects to be catalogued.
+
+    A function takes precedence over object lists.
+    """
+    setSite(site)
+    cat = getUtility(ICatalog, name=cat_name)
+    intids = getUtility(IIntIds, context=cat)
+    if func is not None:
+        objects = func()
+    for ob in objects:
+        doc_id = intids.queryId(ob, None)
+        if doc_id is None:
+            doc_id = intids.register(ob)
+        cat.index_doc(doc_id, ob)
+    return cat
+
+def db_diff(args=None):
+    """Run the analyze tool from ZODB package to find diffs between
+       two ZODBs.
+    """
+    if args is None:
+        args = sys.argv[1:]
+
+    zodb_path1, zodb_path2 = (None, None)
+    if len(args) > 1:
+        zodb_path1, zodb_path2 = args[0:2]
+    else:
+        print
+        print "Analyze two ZODB files and print statistics"
+        print "about contained objects, sizes, etc."
+        print "Shows only differences."
+        print
+        print "Usage: %s <path-to-Data1.fs> <path-to-Data2.fs>" % sys.argv[0]
+        print
+        sys.exit(0)
+
+    a1 = analyze(zodb_path1)
+    a2 = analyze(zodb_path2)
+
+    diff_report(a1, a2)
+
+
+def diff_report(rep1, rep2):
+    print "Processed %d (%d, %d) records in %d (%d, %d) transactions" % (
+        rep1.OIDS + rep2.OIDS, rep1.OIDS, rep2.OIDS,
+        rep1.TIDS + rep2.TIDS, rep2.TIDS, rep2.TIDS)
+    print "Types used:"
+    fmt = "  %-44s %7s %9s %6s %7s"
+    fmtp = "%-44s %+7d %9d %5.1f%% %7.2f" # per-class format
+    fmtpplus = "+ %-44s %+7d %+9d %+5.1f%% %7.2f" # per-class format
+    fmtpminus = "- %-44s %+7d %+9d %+5.1f%% %7.2f" # per-class format
+    fmts = "  %44s %+7d %+8dk %+5.1f%% %+7.2f" # summary format
+    typemap1, typemap2 = rep1.TYPEMAP.keys(), rep2.TYPEMAP.keys()
+    typemap1.sort()
+    typemap2.sort()
+    typemap = list(set(typemap1 + typemap2))
+    typemap.sort()
+    print fmt % ("Class Name", "Count", "TBytes", "Pct", "AvgSize")
+    print fmt % ('-'*44, '-'*7, '-'*9, '-'*5, '-'*7)
+    for t in typemap:
+        if t in typemap1 and t in typemap2 and (
+            rep1.TYPESIZE[t] == rep2.TYPESIZE[t]) and (
+            rep1.TYPEMAP[t] == rep2.TYPEMAP[t]):
+            continue
+        if t not in typemap1:
+            cnt = rep2.TYPEMAP[t]
+            pct = rep2.TYPESIZE[t] * 100.0 / rep2.DBYTES
+            print fmtpplus % (shorten(t, 44), cnt, rep2.TYPESIZE[t],
+                      pct, rep2.TYPESIZE[t] * 1.0 / rep2.TYPEMAP[t])
+
+        elif t not in typemap2:
+            cnt = -rep1.TYPEMAP[t]
+            pct = rep1.TYPESIZE[t] * 100.0 / rep1.DBYTES
+            print fmtpminus % (shorten(t, 44), cnt, -rep1.TYPESIZE[t],
+                      pct, rep1.TYPESIZE[t] * 1.0 / rep1.TYPEMAP[t])
+        else:
+            cnt = rep2.TYPEMAP[t] - rep1.TYPEMAP[t]
+            pct1 = rep1.TYPESIZE[t] * 100.0 / rep1.DBYTES
+            pct2 = rep2.TYPESIZE[t] * 100.0 / rep2.DBYTES
+            pct = pct2 - pct1
+            size = rep2.TYPESIZE[t] - rep1.TYPESIZE[t]
+            if cnt > 0:
+                print fmtpplus % (
+                    shorten(t, 44), cnt, size,
+                    pct, rep2.TYPESIZE[t] * 1.0 / rep2.TYPEMAP[t])
+            else:
+                print fmtpminus % (
+                    shorten(t, 44), cnt, size,
+                    pct, rep1.TYPESIZE[t] * 1.0 / rep1.TYPEMAP[t])
+
+
+    print fmt % ('='*44, '='*7, '='*9, '='*5, '='*7)
+    print fmts % ('Current Objects', rep2.COIDS - rep1.COIDS,
+                  (rep2.CBYTES / 1024.0) - (rep1.CBYTES / 1024.0),
+                  (rep2.CBYTES * 100.0 / rep2.DBYTES) - (
+                      rep1.CBYTES * 100.0 / rep1.DBYTES),
+                  (rep2.CBYTES * 1.0 / rep2.COIDS) - (
+                      rep1.CBYTES * 1.0 / rep1.COIDS))
+    #if rep1.FOIDS and rep2.FOIDS:
+    fbytes = (rep2.FBYTES / 1024.0) - (rep1.FBYTES / 1024.0)
+    rep1_fpct = (rep1.DBYTES and
+                 (rep1.FBYTES * 100.0 / (rep1.DBYTES)) or 0.0)
+    rep2_fpct = (rep2.DBYTES and
+                 (rep2.FBYTES * 100.0 / (rep2.DBYTES)) or 0.0)
+    print fmts % ('Old Objects', rep2.FOIDS - rep1.FOIDS,
+                  fbytes, rep2_fpct - rep1_fpct,
+                  (rep2.FBYTES * 1.0 / (rep2.FOIDS or 1.0)) - (
+                      rep1.FBYTES * 1.0 / (rep1.FOIDS or 1.0)))
+    return
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/__init__.py	(revision 16045)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding temporary jobs (mandates).
+"""
+# Make this a package.
+from waeup.kofa.mandates.container import MandatesContainer
+from waeup.kofa.mandates.mandate import Mandate
+
+__all__ = [
+    'MandatesContainer',
+    'Mandate',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/browser.py	(revision 16045)
@@ -0,0 +1,54 @@
+## $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
+##
+"""Views for mandates.
+"""
+import grok
+from urllib import urlencode
+from waeup.kofa.browser.layout import UtilityView
+from waeup.kofa.interfaces import IUniversity
+from waeup.kofa.interfaces import MessageFactory as _
+
+class MandateView(UtilityView, grok.View):
+    """View to execute mandate.
+    """
+    grok.context(IUniversity)
+    grok.name('mandate')
+    grok.require('waeup.Public')
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        mandate_id = form.get('mandate_id', None)
+        if not mandate_id:
+            (self.msg, self.redirect_path) = (_('Misuse'), '')
+            return
+        mandates = grok.getSite()['mandates']
+        mandate = mandates.get(mandate_id, None)
+        if mandate is None:
+            (self.msg, self.redirect_path) = (_('No mandate.'), '')
+            return
+        (self.msg, self.redirect_path) = mandate.execute()
+        if mandate.REDIRECT_WITH_MANDATE_ID:
+            args = {'mandate_id':mandate_id}
+            self.redirect_path = self.redirect_path + '?%s' % urlencode(args)
+        return
+
+    def render(self):
+        if self.msg:
+            self.flash(self.msg, type='warning')
+        self.redirect(self.url(self.context) + self.redirect_path)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/container.py	(revision 16045)
@@ -0,0 +1,78 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Containers which contain mandate objects.
+"""
+import grok
+from datetime import datetime
+from grok import index
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.mandates.interfaces import IMandatesContainer, IMandate
+
+class MandatesContainer(grok.Container):
+    """This is a container for all kind of mandates.
+    """
+    grok.implements(IMandatesContainer)
+    grok.provides(IMandatesContainer)
+
+    @property
+    def count(self):
+        """Count active and expired mandates.
+        """
+        now = datetime.utcnow()
+        total = len(self)
+        expired = len([i for i in self.values() if i.expires < now])
+        active = total - expired
+        return active, expired, total
+
+    def addMandate(self, mandate):
+        if not IMandate.providedBy(mandate):
+            raise TypeError(
+                'MandateContainers contain only IMandate instances')
+        self[mandate.mandate_id] = mandate
+        return
+
+    def removeExpired(self):
+        """Remove all expired mandates.
+        """
+        num_deleted = 0
+        keys = list(self.keys())
+        for mandate in keys:
+            if self[mandate].expires < datetime.utcnow():
+                del self[mandate]
+                num_deleted += 1
+        grok.getSite().logger.info('%s mandates purged' % num_deleted)
+        return num_deleted
+
+class MandatesPlugin(grok.GlobalUtility):
+    """A plugin that creates container for mandates inside a university.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('mandates')
+
+    def setup(self, site, name, logger):
+        if 'mandates' in site.keys():
+            logger.warn('Could not create container for mandates in Kofa.')
+            return
+        site['mandates'] = MandatesContainer()
+        logger.info('Container for mandates created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'mandates' in site.keys():
+            self.setup(site, name, logger)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/interfaces.py	(revision 16045)
@@ -0,0 +1,48 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+from zope.interface import Attribute
+from zope import schema
+from waeup.kofa.interfaces import IKofaObject
+
+class IMandatesContainer(IKofaObject):
+    """A container for all kind of mandate objects.
+
+    """
+
+    count = Attribute('Count active and expired mandates.')
+
+    def addMandate(mandate):
+        """Add mandate.
+        """
+
+    def removeExpired():
+        """Remove all expired mandates and return the number
+        of successfully removed mandates.
+        """
+
+class IMandate(IKofaObject):
+    """A representation of all mandates.
+
+    """
+    mandate_id = Attribute('Mandate Identifier')
+    expires = Attribute('Expiration Datetime')
+    params = Attribute('Dictionary with mandate parameters')
+
+    def execute():
+        """Method which is performed by the mandate.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/mandate.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/mandate.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/mandate.py	(revision 16045)
@@ -0,0 +1,128 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+These are the mandates.
+"""
+import grok
+import hashlib
+import os
+from datetime import datetime, timedelta
+from grok import index
+from waeup.kofa.interfaces import IUserAccount
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.mandates.interfaces import IMandate
+
+class Mandate(grok.Model):
+    """This is a mandate.
+    """
+    grok.implements(IMandate)
+    grok.provides(IMandate)
+    grok.baseclass()
+
+    REDIRECT_WITH_MANDATE_ID = False
+
+    def __init__(self, days=1, mandate_id=None):
+        super(Mandate, self).__init__()
+        self.creation_date = datetime.utcnow() # offset-naive datetime
+        delta = timedelta(days=days)
+        self.expires = datetime.utcnow() + delta
+        if mandate_id is None:
+            mandate_id = os.urandom(20)
+            mandate_id = hashlib.md5(mandate_id).hexdigest()
+        self.mandate_id = mandate_id
+        self.params = {}
+        return
+
+    def execute(self):
+        return _('Nothing to do.')
+
+class PasswordMandate(Mandate):
+    """This is a mandate which can set a password.
+    """
+
+    def _setPassword(self):
+        user = self.params.get('user', None)
+        pwd = self.params.get('password', None)
+        if not None in (user, pwd):
+            try:
+                IUserAccount(user).setPassword(pwd)
+                return True
+            except:
+                return False
+        return False
+
+    def execute(self):
+        msg = _('Wrong mandate parameters.')
+        redirect_path = ''
+        if self.expires < datetime.utcnow():
+            msg = _('Mandate expired.')
+        elif self._setPassword():
+            msg = _('Password has been successfully set. '
+                    'Login with your new password.')
+            username = IUserAccount(self.params['user']).name
+            grok.getSite().logger.info(
+                'PasswordMandate used: %s ' % username)
+            redirect_path = '/login'
+        del self.__parent__[self.mandate_id]
+        return msg, redirect_path
+
+class ParentsPasswordMandate(Mandate):
+
+    def _setPassword(self):
+        student = self.params.get('student', None)
+        pwd = self.params.get('password', None)
+        if not None in (student, pwd):
+            try:
+                student.setParentsPassword(pwd)
+                return True
+            except:
+                return False
+        return False
+
+    def execute(self):
+        msg = _('Wrong mandate parameters.')
+        redirect_path = ''
+        if self.expires < datetime.utcnow():
+            msg = _('Mandate expired.')
+        elif self._setPassword():
+            msg = _('Parents password has been successfully set. '
+                    'Login with your new parents password.')
+            grok.getSite().logger.info(
+                'ParentsPasswordMandate used: %s '
+                % self.params['student'].student_id)
+            redirect_path = '/login'
+        del self.__parent__[self.mandate_id]
+        return msg, redirect_path
+
+class RefereeReportMandate(Mandate):
+    """This is a mandate which can unlock a `RefereeReportAddFormPage`.
+    The mandate is not automatically deleted. This has to be done
+    by the submit method of the add form page.
+    """
+
+    REDIRECT_WITH_MANDATE_ID = True
+
+    def execute(self):
+        if self.expires < datetime.utcnow():
+            return _('Mandate expired.'), ''
+        redirect_path = self.params.get('redirect_path', None)
+        name = self.params.get('name', None)
+        email = self.params.get('email', None)
+        if None in (redirect_path, name, email):
+            return _('Wrong mandate parameters.'), ''
+        return None, redirect_path
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/tests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/tests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/mandates/tests.py	(revision 16045)
@@ -0,0 +1,302 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""
+Tests for mandates.
+"""
+import tempfile
+import shutil
+import os
+from zope.testbrowser.testing import Browser
+from datetime import datetime, timedelta
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import createObject
+from zope.component.hooks import setSite, clearSite
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IUserAccount
+from waeup.kofa.mandates.interfaces import (
+    IMandatesContainer, IMandate)
+from waeup.kofa.mandates.container import MandatesContainer
+from waeup.kofa.mandates.mandate import (
+    PasswordMandate, RefereeReportMandate, ParentsPasswordMandate)
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+from waeup.kofa.tests.test_authentication import SECRET
+
+class MandatesContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IMandatesContainer, MandatesContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandatesContainer, MandatesContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IMandate, PasswordMandate)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandate, PasswordMandate())
+            )
+        self.assertTrue(
+            verifyClass(
+                IMandate, RefereeReportMandate)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandate, RefereeReportMandate())
+            )
+        self.assertTrue(
+            verifyClass(
+                IMandate, ParentsPasswordMandate)
+            )
+        self.assertTrue(
+            verifyObject(
+                IMandate, ParentsPasswordMandate())
+            )
+        return
+
+    def setUp(self):
+        super(MandatesContainerTestCase, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(MandatesContainerTestCase, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+    def test_set_student_password(self):
+        student = createObject('waeup.Student')
+        # Add and execute a mandate with missing parameters.
+        mandate = PasswordMandate()
+        IUserAccount(student).setPassword('old_pw')
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(redirect_path, '')
+        self.assertEqual(msg, u'Wrong mandate parameters.')
+        # Add and execute an expired mandate.
+        mandate = PasswordMandate(days=0)
+        mandate.params['user'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(msg, u'Mandate expired.')
+        self.assertEqual(redirect_path, '')
+        # Password has not been set
+        self.assertTrue(IUserAccount(student).checkPassword('old_pw'))
+        # Add and execute a perfect mandate
+        mandate = PasswordMandate()
+        mandate.params['user'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg,
+            'Password has been successfully set. Login with your new password.')
+        self.assertEqual(redirect_path, '/login')
+        self.assertTrue(IUserAccount(student).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - PasswordMandate used: K1000000' in logcontent)
+
+    def test_set_officer_password(self):
+        self.app['users'].addUser('bob', SECRET)
+        officer = self.app['users']['bob']
+        mandate = PasswordMandate()
+        mandate.params['user'] = officer
+        mandate.params['password'] = SECRET
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg,
+            'Password has been successfully set. Login with your new password.')
+        self.assertEqual(redirect_path, '/login')
+        self.assertTrue(IUserAccount(officer).checkPassword(SECRET))
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - PasswordMandate used: bob' in logcontent)
+
+    def test_set_applicant_password(self):
+        applicant = createObject('waeup.Applicant')
+        applicant.applicant_id = u'abc'
+        mandate = PasswordMandate()
+        mandate.params['user'] = applicant
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg,
+            'Password has been successfully set. Login with your new password.')
+        self.assertEqual(redirect_path, '/login')
+        self.assertTrue(IUserAccount(applicant).checkPassword('mypwd1'))
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - PasswordMandate used: abc' in logcontent)
+
+    def test_remove_expired(self):
+        # mandate1 is an old mandate which just expired.
+        mandate1 = PasswordMandate(days=0)
+        self.app['mandates'].addMandate(mandate1)
+        # mandate2 is a new mandate with default time delta.
+        mandate2 = PasswordMandate(mandate_id='23456')
+        self.app['mandates'].addMandate(mandate2)
+        self.assertEqual(len(self.app['mandates'].keys()), 2)
+        self.assertEqual(self.app['mandates'].count, (1, 1, 2))
+        num_deleted = self.app['mandates'].removeExpired()
+        self.assertEqual(num_deleted, 1)
+        # Only the new mandate remains in the container.
+        self.assertEqual(len(self.app['mandates'].keys()), 1)
+        self.assertEqual([i for i in self.app['mandates'].keys()], [u'23456'])
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - 1 mandates purged' in logcontent)
+
+    def test_purge_mandates(self):
+        # mandate1 is an old mandate which just expired.
+        mandate1 = PasswordMandate(days=0)
+        self.app['mandates'].addMandate(mandate1)
+        # mandate2 is a new mandate with default time delta.
+        mandate2 = PasswordMandate(mandate_id='23456')
+        self.app['mandates'].addMandate(mandate2)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/configuration')
+        self.assertEqual(self.app['mandates'].count, (1, 1, 2))
+        self.assertTrue('<span>expired</span>' in self.browser.contents)
+        self.browser.getControl("Purge mandates").click()
+        self.assertTrue('1 mandate(s) were purged' in self.browser.contents)
+        self.assertEqual(self.app['mandates'].count, (1, 0, 1))
+
+    def test_browser_set_password(self):
+        student = createObject('waeup.Student')
+        self.app['students'].addStudent(student)
+        mandate = PasswordMandate()
+        mandate.params['user'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        self.browser.open('http://localhost/app/mandate?mandate_id=%s'
+            % mandate.mandate_id)
+        # Password has been set.
+        self.assertTrue('Password has been successfully set. Login with your new password.'
+            in self.browser.contents)
+        self.assertTrue(IUserAccount(student).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+        # We redirect to login page not to the frontpage.
+        self.assertEqual(self.browser.url, 'http://localhost/app/login')
+        # Mandate has been deleted
+        self.browser.open('http://localhost/app/mandate?mandate_id=%s'
+            % mandate.mandate_id)
+        self.assertTrue('No mandate' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+        # Mandate id is needed
+        self.browser.open('http://localhost/app/mandate')
+        self.assertTrue('Misuse' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app')
+
+    def test_refereereport_mandate(self):
+        mandate = RefereeReportMandate()
+        mandate.params['name'] = u'John Referee'
+        mandate.params['email'] = 'aa@aa.aa'
+        mandate.params['redirect_path'] = 'applicants/87689'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(msg, None)
+        self.assertEqual(redirect_path, 'applicants/87689')
+        # Mandate has not been deleted
+        self.assertEqual(len(self.app['mandates'].keys()), 1)
+        mandate.params['redirect_path'] = None
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(msg, 'Wrong mandate parameters.')
+        self.assertEqual(redirect_path, '')
+        # Add and execute an expired mandate
+        mandate2 = RefereeReportMandate(days=0)
+        mandate2.params['name'] = u'John Referee'
+        mandate2.params['email'] = 'aa@aa.aa'
+        mandate2.params['redirect_path'] = 'applicants/87689'
+        self.app['mandates'].addMandate(mandate2)
+        (msg, redirect_path) = mandate2.execute()
+        self.assertEqual(msg, 'Mandate expired.')
+        self.assertEqual(redirect_path, '')
+        # Both mandates still exist
+        self.assertEqual(len(self.app['mandates'].keys()), 2)
+
+    def test_set_parents_password(self):
+        student = createObject('waeup.Student')
+        # Add and execute a mandate with missing parameters.
+        mandate = ParentsPasswordMandate()
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(redirect_path, '')
+        self.assertEqual(msg, u'Wrong mandate parameters.')
+        # Add and execute an expired mandate.
+        mandate = PasswordMandate(days=0)
+        mandate.params['student'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        self.assertEqual(msg, u'Mandate expired.')
+        self.assertEqual(redirect_path, '')
+        # Password has not been set
+        self.assertEqual(student.getParentsPassword(), None)
+        # Add and execute a perfect mandate
+        mandate = ParentsPasswordMandate()
+        mandate.params['student'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        (msg, redirect_path) = mandate.execute()
+        # Parents password has been set and must be used for login.
+        self.assertEqual(msg,
+            'Parents password has been successfully set. '
+            'Login with your new parents password.')
+        self.assertEqual(redirect_path, '/login')
+        self.assertTrue(IUserAccount(student).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - ParentsPasswordMandate used: K1000000'
+                        in logcontent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.py	(revision 16045)
@@ -0,0 +1,124 @@
+## $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
+##
+"""Grokkers for Kofa components.
+
+Stuff in here is mainly taken from grok.meta, with some modifications
+to provide a possibility to update catalogs in a running site. This is
+to solve the very common problem of already running sites for which
+the software changes and adds catalogs. These catalogs will not be
+automatically added in a running site.
+
+With the following Grokker and the upgrade helper class we can fire
+IObjectUpgradedEvents for any existing site and all catalogs
+registered will be really created.
+
+Please note that yet these catalogs are not filled automatically! That
+means the catalogs will exist but are empty in the beginning.
+
+We might solve this second problem by firing extra events like some
+ICatalogUpdatedEvent (which still do not exist) or similar.
+"""
+import grok
+import grokcore.component
+import martian
+from grok import components
+from grok.meta import IndexesSetupSubscriber
+from martian.error import GrokError
+from zope import component
+from zope.processlifetime import IDatabaseOpened
+from waeup.kofa.interfaces import IObjectUpgradeEvent
+
+class IndexesGrokker(martian.InstanceGrokker):
+    """Grokker for Grok index bundles."""
+    martian.component(components.IndexesClass)
+
+    def grok(self, name, factory, module_info, config, **kw):
+        site = grok.site.bind().get(factory)
+        context = grok.context.bind().get(factory, module_info.getModule())
+        catalog_name = grok.name.bind().get(factory)
+
+        if site is None:
+            raise GrokError("No site specified for grok.Indexes "
+                            "subclass in module %r. "
+                            "Use grok.site() to specify."
+                            % module_info.getModule(),
+                            factory)
+        indexes = getattr(factory, '__grok_indexes__', None)
+        if indexes is None:
+            return False
+
+        subscriber = KofaIndexesUpgradeSubscriber(
+            catalog_name, indexes, context, module_info)
+        subscribed = (site, IObjectUpgradeEvent)
+        config.action(
+            discriminator=None,
+            callable=component.provideHandler,
+            args=(subscriber, subscribed),
+            )
+        return True
+
+
+class KofaIndexesUpgradeSubscriber(IndexesSetupSubscriber):
+    """Helper that sets up indexes when their Grok site is upgraded.
+
+    Each `grok.Indexes` class serves as an assertion that, whenever an
+    instance of its `grok.site()` is upgraded, the given list of
+    indexes should be generated if not already created as well.  But a
+    long period of time could elapse between when the application
+    starts (and its indexes are grokked), and the moment, maybe days
+    or weeks later, when a new instance of that `grok.Site` is
+    created.  Hence this `IndexesSetupSubscriber`: it can be
+    instantiated at grokking time with the index information, and then
+    registered with the Component Architecture as an event that should
+    be fired later, whenever the right kind of `grok.Site` is
+    instantiated.  At that point its `__call__` method is kicked off
+    and it makes sure the index catalogs get created properly.
+
+    """
+    def __call__(self, site, event):
+        #site.logger.info('Create catalog `%s` if not installed yet.'  % (
+        #        self.catalog_name,))
+        # make sure we have an intids
+        self._createIntIds(site)
+        # get the catalog
+        catalog = self._createCatalog(site)
+        # now install indexes
+        for name, index in self.indexes.items():
+            try:
+                #site.logger.info('Create index `%s` in catalog.' % name)
+                index.setup(catalog, name, self.context, self.module_info)
+                site.logger.info('%s: Index %s created.' % (self.catalog_name,name))
+            except KeyError: #, DuplicationError:
+                #site.logger.info('Index `%s` already in catalog.' % name)
+                pass
+        #site.logger.info('Catalog `%s` ready.' % self.catalog_name)
+
+
+
+@grokcore.component.subscribe(IDatabaseOpened)
+def disable_broken_handlers(event):
+    """This one is fired after ZODB setup.
+
+    Unregsiter handlers etc., that are broken or meant trouble in the past.
+    """
+    from zope.component import getGlobalSiteManager
+    from zope.catalog.catalog import indexAdded
+    worked = getGlobalSiteManager().unregisterHandler(indexAdded)
+    if worked:
+        print("Unregistered handler zope.catalog.catalog.indexAdded")
+    # TODO: register a replacement that copes better with large indexes.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/meta.zcml	(revision 16045)
@@ -0,0 +1,20 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    xmlns:grok="http://namespaces.zope.org/grok">
+
+  <include package="grok" file="meta.zcml" />
+  <!-- include package="z3c.autoinclude" file="meta.zcml" / -->
+
+  <!-- Load the grokkers -->
+  <grok:grok package=".meta" />
+
+  <meta:directive
+      namespace="http://namespaces.waeup.org/kofa"
+      name="datacenter"
+      schema=".zcml.IDataCenterConfig"
+      handler=".zcml.data_center_conf"
+      />
+
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/objecthistory.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/objecthistory.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/objecthistory.py	(revision 16045)
@@ -0,0 +1,113 @@
+## $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
+##
+import grok
+from datetime import datetime
+from persistent.list import PersistentList
+from zope.component import getUtility
+from zope.i18n import translate
+from zope.annotation.interfaces import IAnnotations
+from waeup.kofa.interfaces import IObjectHistory, IKofaObject, IKofaUtils
+from waeup.kofa.utils.helpers import get_current_principal, now
+
+from waeup.kofa.interfaces import MessageFactory as _
+
+class ObjectHistory(grok.Adapter):
+    """A history for objects.
+
+    For any object implementing `IKofaObject` which is annotatable,
+    we provide histories. A history for such an object can be obtained
+    by adapting it to `IObjectHistory`.
+    """
+    grok.context(IKofaObject)
+    grok.implements(IObjectHistory)
+
+    history_key = 'waeup.history'
+
+    def __init__(self, context):
+        from zope.security.proxy import removeSecurityProxy
+        self.context = removeSecurityProxy(context)
+        self._annotations = IAnnotations(self.context)
+
+    def _getMessages(self):
+        return self._annotations.get(self.history_key, PersistentList())
+
+    @property
+    def messages(self):
+        """Get all messages as a persistent list of strings.
+        """
+        return self._getMessages()
+
+    def addMessage(self, msg, user=None):
+        """Add the message (history entry) in msg.
+
+        Any message will be stored with a timestamp and the current
+        user (principal) if user parameter is None.
+        """
+        msgs = self._getMessages()
+        tz = getUtility(IKofaUtils).tzinfo
+        timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        msg = translate(msg,'waeup.kofa',target_language=portal_language)
+        by = translate(_('by'),'waeup.kofa',target_language=portal_language)
+        if user == None:
+            user = get_current_principal()
+            if user is None:
+                usertitle = 'system'
+            elif user.id == 'zope.anybody':
+                usertitle = 'Anonymous'
+            else:
+                usertitle = getattr(user, 'public_name', None)
+                if not usertitle:
+                    usertitle = user.title
+            msg = u'%s - %s %s %s' % (timestamp, msg, by, usertitle)
+        elif user == 'undisclosed':
+            msg = u'%s - %s' % (timestamp, msg)
+        else:
+            msg = u'%s - %s %s %s' % (timestamp, msg, by, user)
+        msgs.append(msg)
+        self._annotations[self.history_key] = msgs
+        return
+
+    def modifyMessages(self, old, new):
+        """Replaces history messages.
+
+        Substring 'old' will be replaced by 'new' in all
+        messages.
+        """
+        old_msgs = self._getMessages()
+        new_msgs = PersistentList()
+        for msg in old_msgs:
+            new_msg = msg.replace(old, new)
+            new_msgs.append(new_msg)
+        self._annotations[self.history_key] = new_msgs
+        return
+
+    def removeMessage(self, number):
+        """Removes a single history message.
+
+        """
+        msgs = self._getMessages()
+        if not isinstance(number, int):
+            return False, 'Not a number'
+        try:
+            line = msgs[number]
+        except IndexError:
+            return False, 'Number out of range'
+        msgs.pop(number)
+        self._annotations[self.history_key] = msgs
+        return True, line
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/__init__.py	(revision 16045)
@@ -0,0 +1,10 @@
+"""This package contains everything regarding payments.
+"""
+# Make this a package.
+from waeup.kofa.payments.container import PaymentsContainer
+from waeup.kofa.payments.payment import OnlinePayment
+
+__all__ = [
+    'PaymentsContainer',
+    'OnlinePayment',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/catalog.py	(revision 16045)
@@ -0,0 +1,35 @@
+## $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
+##
+"""Components to help cataloging and searching payments.
+"""
+import grok
+from waeup.kofa.interfaces import IUniversity
+from waeup.kofa.payments.interfaces import IPayment
+
+class PaymentIndexes(grok.Indexes):
+    """A catalog for all payments.
+    """
+    grok.site(IUniversity)
+    grok.name('payments_catalog')
+    grok.context(IPayment)
+
+    p_id = grok.index.Field(attribute='p_id')
+    p_session = grok.index.Field(attribute='p_session')
+    p_category = grok.index.Field(attribute='p_category')
+    p_item = grok.index.Field(attribute='p_item')
+    p_state = grok.index.Field(attribute='p_state')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/container.py	(revision 16045)
@@ -0,0 +1,42 @@
+## $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
+##
+"""
+Containers which contain payment objects.
+"""
+import grok
+from grok import index
+from waeup.kofa.payments.interfaces import IPaymentsContainer
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class PaymentsContainer(grok.Container):
+    """This is a container for all kind of payments.
+    """
+    grok.implements(IPaymentsContainer)
+    grok.provides(IPaymentsContainer)
+
+    def __init__(self):
+        super(PaymentsContainer, self).__init__()
+        return
+
+    def archive(self, id=None):
+        raise NotImplementedError()
+
+    def clear(self, id=None, archive=True):
+        raise NotImplementedError()
+
+PaymentsContainer = attrs_to_fields(PaymentsContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/interfaces.py	(revision 16045)
@@ -0,0 +1,168 @@
+## $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
+##
+from zope.interface import Attribute
+from zope import schema
+from waeup.kofa.interfaces import (
+    IKofaObject, SimpleKofaVocabulary, academic_sessions_vocab,
+    ContextualDictSourceFactoryBase)
+from waeup.kofa.interfaces import MessageFactory as _
+
+payment_states = SimpleKofaVocabulary(
+    (_('Not yet paid'),'unpaid'),
+    (_('Paid'),'paid'),
+    (_('Failed'),'failed'),
+    (_('Fees waived'),'waived'),
+    (_('Scholarship'),'scholarship'),
+    )
+
+class PaymentCategorySource(ContextualDictSourceFactoryBase):
+    """A payment category source delivers all categories of payments.
+
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'PAYMENT_CATEGORIES'
+
+class CombiPaymentCategorySource(ContextualDictSourceFactoryBase):
+    """A payment category source delivers all categories of payments.
+
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'COMBI_PAYMENT_CATEGORIES'
+
+class IPaymentsContainer(IKofaObject):
+    """A container for all kind of payment objects.
+
+    """
+
+class IPayment(IKofaObject):
+    """A base representation of payments.
+
+    """
+    p_id = Attribute('Payment identifier')
+
+    p_category = schema.Choice(
+        title = _(u'Payment Category'),
+        #default = u'schoolfee',
+        source = PaymentCategorySource(),
+        required = True,
+        )
+
+    p_item = schema.TextLine(
+        title = u'',
+        default = None,
+        required = False,
+        )
+
+    p_combi = schema.List(
+        title = _(u'Combi Payment'),
+        value_type = schema.Choice(
+            source = CombiPaymentCategorySource(),
+            ),
+        required = False,
+        defaultFactory=list,
+        )
+
+    display_item = schema.TextLine(
+        title = _(u'Payment Item'),
+        required = False,
+        readonly = True,
+        )
+
+    p_session = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    p_state = schema.Choice(
+        title = _(u'Payment State'),
+        default = u'unpaid',
+        vocabulary = payment_states,
+        required = True,
+        )
+
+    creation_date = schema.Datetime(
+        title = _(u'Ticket Creation Date'),
+        readonly = False,
+        required = False,
+        )
+
+    payment_date = schema.Datetime(
+        title = _(u'Payment Date'),
+        required = False,
+        readonly = False,
+        )
+
+    amount_auth = schema.Float(
+        title = _(u'Amount Authorized'),
+        default = 0.0,
+        required = True,
+        readonly = False,
+        )
+
+class IOnlinePayment(IPayment):
+    """A payment via payment gateways.
+
+    """
+
+    ac = schema.TextLine(
+        title = _(u'Activation Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_amount_approved = schema.Float(
+        title = _(u'Response Amount Approved'),
+        default = 0.0,
+        required = False,
+        readonly = False,
+        )
+
+    r_code = schema.TextLine(
+        title = _(u'Response Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_desc = schema.TextLine(
+        title = _(u'Response Description'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    def approve():
+        "Approve an online payment and set to paid."
+
+class IPayer(IKofaObject):
+    """An interface for an adapter to publish student and applicant data
+    through a simple webservice.
+
+    """
+    display_fullname = Attribute('Name of  payer')
+    id = Attribute('Id of payer')
+    reg_number = Attribute('Reg number of payer')
+    matric_number = Attribute('Matric number of payer')
+    faculty = Attribute('Faculty of payer')
+    department = Attribute('Department of payer')
+    email= Attribute('Email of payer')
+    phone= Attribute('Phone number of payer')
+    current_mode= Attribute('Current study mode of payer')
+    current_level= Attribute('Current level of payer')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/payment.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/payment.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/payment.py	(revision 16045)
@@ -0,0 +1,105 @@
+## $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
+##
+"""
+These are the payment tickets.
+"""
+import grok
+from datetime import datetime
+from grok import index
+from zope.event import notify
+from zope.component import getUtility
+from zope.i18n import translate
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.payments.interfaces import (
+    IPayment, IOnlinePayment,
+    payment_states)
+from waeup.kofa.utils.helpers import attrs_to_fields, get_current_principal
+from waeup.kofa.utils.logger import Logger
+
+class Payment(grok.Container, Logger):
+    """This is a payment.
+    """
+    grok.implements(IPayment)
+    grok.provides(IPayment)
+    grok.baseclass()
+
+    logger_name = 'waeup.kofa.${sitename}.payments'
+    logger_filename = 'payments.log'
+    logger_format_str = '"%(asctime)s","%(user)s",%(message)s'
+
+    #def logger_info(self, comment=None):
+    #    """Get the logger's info method.
+    #    """
+    #    self.logger.info('%s' % comment)
+    #    return
+
+    def __init__(self):
+        super(Payment, self).__init__()
+        self.creation_date = datetime.utcnow()
+        self.p_id = None
+        return
+
+    @property
+    def p_state_title(self):
+        return payment_states.getTermByToken(self.p_state).title
+
+    @property
+    def category(self):
+        utils = getUtility(IKofaUtils)
+        return utils.PAYMENT_CATEGORIES.get(self.p_category, None)
+
+    @property
+    def display_item(self):
+        kofa_utils = getUtility(IKofaUtils)
+        return kofa_utils.getPaymentItem(self)
+
+class OnlinePayment(Payment):
+    """This is an online payment.
+    """
+    grok.implements(IOnlinePayment)
+    grok.provides(IOnlinePayment)
+
+    def __init__(self):
+        super(OnlinePayment, self).__init__()
+        p_id = None
+        return
+
+    def approve(self):
+        "Approve online payment and set to paid."
+        self.r_amount_approved = self.amount_auth
+        self.r_code = u'AP'
+        self.p_state = 'paid'
+        user = get_current_principal()
+        if user is None:
+            # in tests
+            usertitle = 'system'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+        r_desc = _('Payment approved by ${a}', mapping = {'a': usertitle})
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.r_desc = translate(r_desc, 'waeup.kofa',
+            target_language=portal_language)
+        self.payment_date = datetime.utcnow()
+        # Update catalog
+        notify(grok.ObjectModifiedEvent(self))
+        return
+
+OnlinePayment = attrs_to_fields(OnlinePayment, omit=['display_item'])
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/tests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/tests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/payments/tests.py	(revision 16045)
@@ -0,0 +1,58 @@
+## $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
+##
+"""
+Tests for payments.
+"""
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.payments.interfaces import (
+    IPaymentsContainer, IOnlinePayment)
+from waeup.kofa.payments.container import PaymentsContainer
+from waeup.kofa.payments.payment import OnlinePayment
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+
+class PaymentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IPaymentsContainer, PaymentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IPaymentsContainer, PaymentsContainer())
+            )
+        self.assertTrue(
+            verifyClass(
+                IOnlinePayment, OnlinePayment)
+            )
+        self.assertTrue(
+            verifyObject(
+                IOnlinePayment, OnlinePayment())
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = PaymentsContainer()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/permissions.py	(revision 16045)
@@ -0,0 +1,803 @@
+# $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
+#
+import grok
+from zope.component import getUtilitiesFor
+from zope.interface import Interface
+from zope.securitypolicy.interfaces import IRole, IPrincipalRoleMap
+from waeup.kofa.interfaces import ILocalRolesAssignable
+
+
+class Public(grok.Permission):
+    """The Public or everyone-can-do-this-permission is being applied
+    to views/pages that are used by everyone.
+    """
+    grok.name('waeup.Public')
+
+
+class Anonymous(grok.Permission):
+    """The Anonymous permission is applied to
+    views/pages which are dedicated to anonymous users only.
+    Logged-in users can't access these views.
+    """
+    grok.name('waeup.Anonymous')
+
+
+class Authenticated(grok.Permission):
+    """The Authenticated permission is applied to pages
+    which can only be used by logged-in users and not by anonymous users.
+    """
+    grok.name('waeup.Authenticated')
+
+
+class ViewAcademics(grok.Permission):
+    """The ViewAcademics permission is applied to all
+    views of the Academic Section. Users with this permission can view but
+    not edit content in the Academic Section.
+    """
+    grok.name('waeup.viewAcademics')
+
+
+class ManageAcademics(grok.Permission):
+    """The ManageAcademics permission is applied to all edit/manage
+    pages in the Academic Section. Users who have this permission
+    can change/edit context objects.
+    """
+    grok.name('waeup.manageAcademics')
+
+
+class ManagePortal(grok.Permission):
+    """The ManagePortal permission is used for very few pages
+    (e.g. the DatacenterSettings page). Only PortalManagers have this
+    permission. It is furthermore used to control delete methods of container
+    pages in the Academic Section. The ManageAcademics permission,
+    described above, does enable users to edit content but not to
+    remove sub-containers, like faculties, departments or certificates.
+    Users must have the ManagePortal permission too to remove
+    entire containers.
+    """
+    grok.name('waeup.managePortal')
+
+
+class ManageUsers(grok.Permission):
+    """The ManageUsers permission is a real superuser permission
+    and therefore very 'dangerous'. It allows to add, remove or edit
+    user accounts. Editing a user account includes the option to assign
+    or remove roles. That means that a user with this permission can lock out
+    other users by either removing their account or by removing
+    permissions.
+    """
+    grok.name('waeup.manageUsers')
+
+
+class ShowStudents(grok.Permission):
+    """Users with this permission do not neccessarily see the 'Students' tab
+    but they can search for students at department, certificate or course
+    level. If they additionally have the ExportData permission they can
+    export the data as csv files.
+
+    Bursary or Department Officers don't have the ExportData
+    permission (see Roles section) and are only allowed to export bursary
+    or payments overview data respectively.
+    """
+    grok.name('waeup.showStudents')
+
+
+class ClearAllStudents(grok.Permission):
+    """The ClearAllStudents permission allows to clear all students
+    in a department at one sweep.
+    """
+    grok.name('waeup.clearAllStudents')
+
+
+class EditScores(grok.Permission):
+    """The EditScores permission allows to edit scores in course tickets.
+    """
+    grok.name('waeup.editScores')
+
+
+class TriggerTransition(grok.Permission):
+    """The TriggerTransition permission allows to trigger workflow transitions
+    of student and document objects.
+    """
+    grok.name('waeup.triggerTransition')
+
+
+class EditUser(grok.Permission):
+    """The EditUser permission is required for editing
+    single user accounts.
+    """
+    grok.name('waeup.editUser')
+
+
+class ManageDataCenter(grok.Permission):
+    """The ManageDataCenter permission allows to access all pages
+    in the Data Center and to upload files. It does not automatically
+    allow to process uploaded data files.
+    """
+    grok.name('waeup.manageDataCenter')
+
+
+class ImportData(grok.Permission):
+    """The ImportData permission allows to batch process (import) any kind of
+    portal data except for user data. The User Data processor
+    requires also the ManageUsers permission.
+    """
+    grok.name('waeup.importData')
+
+
+class ExportData(grok.Permission):
+    """The ExportData permission allows to export any kind of portal data.
+    """
+    grok.name('waeup.exportData')
+
+
+class ExportPaymentsOverview(grok.Permission):
+    """Department Officers don't have the general exportData
+    permission and are only allowed to export payments overviews.
+    The ExportPaymentsOverview permission is only used to filter the
+    respective exporters in the ExportJobContainerJobConfig view.
+    """
+    grok.name('waeup.exportPaymentsOverview')
+
+
+class ExportBursaryData(grok.Permission):
+    """Bursary Officers don't have the general exportData
+    permission and are only allowed to export bursary data.
+    The ExportBursaryData permission is only used to filter the
+    respective exporter in the ExportJobContainerJobConfig view.
+    """
+    grok.name('waeup.exportBursaryData')
+
+
+class ManagePortalConfiguration(grok.Permission):
+    """The ManagePortalConfiguration permission allows to
+    edit global and sessional portal configuration data.
+    """
+    grok.name('waeup.managePortalConfiguration')
+
+
+class ManageACBatches(grok.Permission):
+    """The ManageACBatches permission allows to view and
+    manage accesscodes.
+    """
+    grok.name('waeup.manageACBatches')
+
+
+class PutBiometricDataPermission(grok.Permission):
+    """This permission allows to upload/change biometric data.
+    """
+    grok.name('waeup.putBiometricData')
+
+
+class GetBiometricDataPermission(grok.Permission):
+    """This permission allows to read biometric data.
+    """
+    grok.name('waeup.getBiometricData')
+
+
+# Local Roles
+
+class ApplicationsManager(grok.Role):
+    """The local ApplicationsManager role can be assigned at applicants
+    container and at department level. At department level an Applications
+    Manager can manage all applicants which desire to study a programme
+    offered by the department (1st Choice Course of Study).
+
+    At container level (local) Applications Managers gain permissions which
+    allow to manage the container and all applicants inside the container.  At
+    container level the permission set of this local role corresonds with the
+    permission set of the same-named global role.
+    """
+    grok.name('waeup.local.ApplicationsManager')
+    grok.title(u'Applications Manager')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.manageApplication', 'waeup.viewApplication',
+                     'waeup.payApplicant')
+
+
+class DepartmentOfficer(grok.Role):
+    """The local DepartmentOfficer role can be assigned at faculty or
+    department level. The role allows to list all student data within the
+    faculty/department the local role is assigned. And it allows to export
+    payment data overviews.
+    """
+    grok.name('waeup.local.DepartmentOfficer')
+    grok.title(u'Department Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportPaymentsOverview')
+
+
+class DepartmentManager(grok.Role):
+    """The local DepartmentManager role can be assigned at faculty or
+    department level. The role allows to edit all data within this container.
+    It does not automatically allow to remove sub-containers.
+
+    Department Managers (Dean of Faculty or Head of Department respectively)
+    can also list student data but not access student pages.
+    """
+    grok.name('waeup.local.DepartmentManager')
+    grok.title(u'Department Manager')
+    grok.permissions('waeup.manageAcademics',
+                     'waeup.showStudents',
+                     'waeup.exportData')
+
+
+class ClearanceOfficer(grok.Role):
+    """The local ClearanceOfficer role can be assigned at faculty or
+    department level. The role allows to list or export all student
+    data within the faculty/department the local role is assigned.
+
+    Clearance Officers can furthermore clear all students or reject clearance
+    of all students in their faculty/department. They get the
+    StudentsClearanceOfficer role for this subset of students.
+    """
+    grok.name('waeup.local.ClearanceOfficer')
+    grok.title(u'Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData',
+                     'waeup.clearAllStudents')
+
+
+class LocalStudentsManager(grok.Role):
+    """The local LocalStudentsManager role can be assigned at faculty or
+    department level. The role allows to view all data and to view or export
+    all student data within the faculty/department the local role is assigned.
+
+    Local Students Managers can furthermore manage data of students
+    in their faculty/department. They get the StudentsManager role for
+    this subset of students.
+    """
+    grok.name('waeup.local.LocalStudentsManager')
+    grok.title(u'Students Manager')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class LocalWorkflowManager(grok.Role):
+    """The local LocalWorkflowManager role can be assigned at faculty level.
+    The role allows to view all data and to list or export
+    all student data within the faculty the local role is assigned.
+
+    Local Workflow Managers can trigger transition of students in their
+    faculty/department. They get the WorkflowManager role for
+    this subset of students.
+    """
+    grok.name('waeup.local.LocalWorkflowManager')
+    grok.title(u'Student Workflow Manager')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class LocalReportsOfficer(grok.Role):
+    """The local ReportsOfficer role can be assigned at department level.
+    The role allows to view all data and to list or export
+    all student data within the department the local role is assigned.
+
+    The LocalReportsOfficer requires the assignment of the global
+    ReportsOfficer role to access the reports section. If set, it reduces
+    the number of available report generators and selectable certificates.
+    Local Reports Officers can create only reports for their department.
+    """
+    grok.name('waeup.local.ReportsOfficer')
+    grok.title(u'Student Reports Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class UGClearanceOfficer(grok.Role):
+    """UG Clearance Officers are regular Clearance Officers with restricted
+    dynamic permission assignment. They can only access undergraduate
+    students.
+    """
+    grok.name('waeup.local.UGClearanceOfficer')
+    grok.title(u'UG Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData',
+                     'waeup.clearAllStudents')
+
+
+class PGClearanceOfficer(grok.Role):
+    """PG Clearance Officers are regular Clearance Officers with restricted
+    dynamic permission assignment. They can only access postgraduate
+    students.
+    """
+    grok.name('waeup.local.PGClearanceOfficer')
+    grok.title(u'PG Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData',
+                     'waeup.clearAllStudents')
+
+
+class CourseAdviser100(grok.Role):
+    """The local CourseAdviser100 role can be assigned at faculty,
+    department or certificate level. The role allows to view all data and
+    to list or export all student data within the faculty, department
+    or certificate the local role is assigned.
+
+    Local Course Advisers can validate or reject course lists of students
+    in ther faculty/department/certificate at level 100.
+    They get the StudentsCourseAdviser role for this subset of students.
+    """
+    grok.name('waeup.local.CourseAdviser100')
+    grok.title(u'Course Adviser 100L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser200(grok.Role):
+    """Same as CourseAdviser100 but for level 200.
+    """
+    grok.name('waeup.local.CourseAdviser200')
+    grok.title(u'Course Adviser 200L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser300(grok.Role):
+    """Same as CourseAdviser100 but for level 300.
+    """
+    grok.name('waeup.local.CourseAdviser300')
+    grok.title(u'Course Adviser 300L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser400(grok.Role):
+    """Same as CourseAdviser100 but for level 400.
+    """
+    grok.name('waeup.local.CourseAdviser400')
+    grok.title(u'Course Adviser 400L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser500(grok.Role):
+    """Same as CourseAdviser100 but for level 500.
+    """
+    grok.name('waeup.local.CourseAdviser500')
+    grok.title(u'Course Adviser 500L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser600(grok.Role):
+    """Same as CourseAdviser100 but for level 600.
+    """
+    grok.name('waeup.local.CourseAdviser600')
+    grok.title(u'Course Adviser 600L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser700(grok.Role):
+    """Same as CourseAdviser100 but for level 700.
+    """
+    grok.name('waeup.local.CourseAdviser700')
+    grok.title(u'Course Adviser 700L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class CourseAdviser800(grok.Role):
+    """Same as CourseAdviser100 but for level 800.
+    """
+    grok.name('waeup.local.CourseAdviser800')
+    grok.title(u'Course Adviser 800L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class LocalTranscriptOfficer(grok.Role):
+    """The LocalTranscriptOfficer role can be assigned at faculty
+    level. The role allows to view, to validate and to
+    release student transcripts at faculty level.
+    Local Transcript Officers get the TranscriptOfficer role
+    for this subset of students.
+    """
+    grok.name('waeup.local.TranscriptOfficer')
+    grok.title(u'Transcript Officer')
+    grok.permissions('waeup.viewAcademics',)
+
+
+class LocalTranscriptSignee(grok.Role):
+    """The LocalTranscriptSignee role can be assigned at faculty
+    level. The role allows to view and to sign student transcripts
+    at faculty level. Local Transcript Signees  get the TranscriptSignee role
+    for this subset of students.
+    """
+    grok.name('waeup.local.TranscriptSignee')
+    grok.title(u'Transcript Signee')
+    grok.permissions('waeup.viewAcademics',)
+
+
+class Lecturer(grok.Role):
+    """The local Lecturer role can be assigned at course level.
+    The role allows to export some student
+    data within the course the local role is assigned. Lecturers can't access
+    student data directly but they can edit the scores in course tickets.
+    """
+    grok.name('waeup.local.Lecturer')
+    grok.title(u'Lecturer')
+    grok.permissions('waeup.editScores',
+                     'waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+
+class Owner(grok.Role):
+    """Each user 'owns' her/his user object and gains permission to edit
+    some of the user attributes.
+    """
+    grok.name('waeup.local.Owner')
+    grok.title(u'Owner')
+    grok.permissions('waeup.editUser')
+
+
+# Site Roles
+class AcademicsOfficer(grok.Role):
+    """An Academics Officer can view but not edit data in the
+    academic section.
+
+    This is the default role which is automatically assigned to all
+    officers of the portal. A user with this role can access all display pages
+    at faculty, department, course, certificate and certificate course level.
+    """
+    grok.name('waeup.AcademicsOfficer')
+    grok.title(u'Academics Officer (view only)')
+    grok.permissions('waeup.viewAcademics')
+
+
+class AcademicsManager(grok.Role):
+    """An Academics Manager can view and edit all data in the
+    scademic section, i.e. access all manage pages
+    at faculty, department, course, certificate and certificate course level.
+    """
+    grok.name('waeup.AcademicsManager')
+    grok.title(u'Academics Manager')
+    title = u'Academics Manager'
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.manageAcademics')
+
+
+class ACManager(grok.Role):
+    """This is the role for Access Code Managers.
+    An AC Manager can view and manage the Accesscodes Section, see
+    ManageACBatches permission above.
+    """
+    grok.name('waeup.ACManager')
+    grok.title(u'Access Code Manager')
+    grok.permissions('waeup.manageACBatches')
+
+
+class DataCenterManager(grok.Role):
+    """This single-permission role is dedicated to those users
+    who are charged with batch processing of portal data.
+    A Data Center Manager can access all pages in the Data Center,
+    see ManageDataCenter permission above.
+    """
+    grok.name('waeup.DataCenterManager')
+    grok.title(u'Datacenter Manager')
+    grok.permissions('waeup.manageDataCenter')
+
+
+class ImportManager(grok.Role):
+    """An Import Manager is a Data Center Manager who is also allowed
+    to batch process (import) data. All batch processors (importers) are
+    available except for the User Processor. This processor requires the
+    Users Manager role too. The ImportManager role includes the
+    DataCenterManager role but not vice versa.
+    """
+    grok.name('waeup.ImportManager')
+    grok.title(u'Import Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.importData')
+
+
+class ExportManager(grok.Role):
+    """An Export Manager is a Data Center Manager who is also allowed
+    to export all kind of portal data. The ExportManager role includes the
+    DataCenterManager role but not vice versa.
+    """
+    grok.name('waeup.ExportManager')
+    grok.title(u'Export Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.exportData',
+                     'waeup.showStudents')
+
+
+class BursaryOfficer(grok.Role):
+    """Bursary Officers can export bursary and payments data. They can't  
+    access the Data Center but see student data export buttons in the
+    Academic Section.
+    """
+    grok.name('waeup.BursaryOfficer')
+    grok.title(u'Bursary Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportBursaryData',
+                     'waeup.exportPaymentsOverview',
+                     'waeup.viewApplicantsTab')
+
+
+class UsersManager(grok.Role):
+    """A Users Manager can add, remove or edit
+    user accounts, see ManageUsers permission for further information.
+    Be very careful with this role.
+    """
+    grok.name('waeup.UsersManager')
+    grok.title(u'Users Manager')
+    grok.permissions('waeup.manageUsers',
+                     'waeup.editUser')
+
+
+class WorkflowManager(grok.Role):
+    """The Workflow Manager can trigger workflow transitions
+    of student and document objects, see TriggerTransition permission
+    for further information.
+    """
+    grok.name('waeup.WorkflowManager')
+    grok.title(u'Workflow Manager')
+    grok.permissions('waeup.triggerTransition')
+
+
+class FingerprintReaderDeviceRole(grok.Role):
+    """Fingerprint Reader Devices.
+
+    Fingerprint readers are remote devices that can store and retrieve
+    fingerprint data.
+    """
+    grok.name('waeup.FingerprintDevice')
+    grok.title(u'Fingerprint Reader')
+    grok.permissions(
+        'waeup.getBiometricData',
+        'waeup.putBiometricData',
+    )
+
+
+class PortalManager(grok.Role):
+    """The PortalManager role is the maximum set of Kofa permissions
+    which are needed to manage the entire portal. This set must not
+    be customized. It is recommended to assign this role only
+    to a few certified Kofa administrators.
+    A less dangerous manager role is the CCOfficer role described below.
+    For the most tasks the CCOfficer role is sufficient.
+    """
+    grok.name('waeup.PortalManager')
+    grok.title(u'Portal Manager')
+    grok.permissions('waeup.managePortal',
+                     'waeup.manageUsers',
+                     'waeup.viewAcademics', 'waeup.manageAcademics',
+                     'waeup.manageACBatches',
+                     'waeup.manageDataCenter',
+                     'waeup.importData',
+                     'waeup.exportData',
+                     'waeup.viewTranscript',
+                     'waeup.processTranscript',
+                     'waeup.viewDocuments', 'waeup.manageDocuments',
+                     'waeup.managePortalConfiguration',
+                     'waeup.viewApplication',
+                     'waeup.manageApplication', 'waeup.handleApplication',
+                     'waeup.viewApplicantsTab', 'waeup.payApplicant',
+                     'waeup.viewApplicationStatistics',
+                     'waeup.viewStudent', 'waeup.manageStudent',
+                     'waeup.clearStudent', 'waeup.payStudent',
+                     'waeup.clearStudentFinancially',  # not used in base pkg
+                     'waeup.uploadStudentFile', 'waeup.showStudents',
+                     'waeup.clearAllStudents',
+                     'waeup.createStudents',
+                     'waeup.editScores',
+                     'waeup.triggerTransition',
+                     'waeup.validateStudent',
+                     'waeup.viewStudentsContainer',
+                     'waeup.handleAccommodation',
+                     'waeup.viewHostels', 'waeup.manageHostels',
+                     'waeup.editUser',
+                     'waeup.loginAsStudent',
+                     'waeup.handleReports',
+                     'waeup.manageReports',
+                     'waeup.manageJobs',
+                     )
+
+
+class CCOfficer(grok.Role):
+    """The role of the Computer Center Officer is basically a copy
+    of the the PortalManager role. Some 'dangerous' permissions are excluded
+    by commenting them out (see source code). If officers need to gain more
+    access rights than defined in this role, do not hastily switch to the
+    PortalManager role but add further manager roles instead. Additional
+    roles could be: UsersManager, ACManager, ImportManager, WorkflowManager
+    or StudentImpersonator.
+
+    CCOfficer is a base class which means that this role is subject to
+    customization. It is not used in the ``waeup.kofa`` base package.
+    """
+    grok.baseclass()
+    grok.name('waeup.CCOfficer')
+    grok.title(u'Computer Center Officer')
+    grok.permissions(
+        # 'waeup.managePortal',
+        # 'waeup.manageUsers',
+        'waeup.viewAcademics',
+        'waeup.manageAcademics',
+        # 'waeup.manageACBatches',
+        'waeup.manageDataCenter',
+        # 'waeup.importData',
+        'waeup.exportData',
+        'waeup.viewTranscript',
+        'waeup.processTranscript',
+        'waeup.viewDocuments', 'waeup.manageDocuments',
+        'waeup.managePortalConfiguration', 'waeup.viewApplication',
+        'waeup.manageApplication', 'waeup.handleApplication',
+        'waeup.viewApplicantsTab', 'waeup.payApplicant',
+        'waeup.viewApplicationStatistics',
+        'waeup.viewStudent', 'waeup.manageStudent',
+        'waeup.clearStudent', 'waeup.payStudent',
+        'waeup.uploadStudentFile', 'waeup.showStudents',
+        'waeup.clearAllStudents',
+        # 'waeup.createStudents',
+        'waeup.editScores',
+        # 'waeup.triggerTransition',
+        'waeup.validateStudent',
+        'waeup.viewStudentsContainer',
+        'waeup.handleAccommodation',
+        'waeup.viewHostels', 'waeup.manageHostels',
+        # 'waeup.editUser',
+        # 'waeup.loginAsStudent',
+        'waeup.handleReports',
+        'waeup.manageReports',
+        # 'waeup.manageJobs',
+        )
+
+
+def get_all_roles():
+    """Return a list of tuples ``<ROLE-NAME>, <ROLE>``.
+    """
+    return getUtilitiesFor(IRole)
+
+
+def get_waeup_roles(also_local=False):
+    """Get all Kofa roles.
+
+    Kofa roles are ordinary roles whose id by convention starts with
+    a ``waeup.`` prefix.
+
+    If `also_local` is ``True`` (``False`` by default), also local
+    roles are returned. Local Kofa roles are such whose id starts
+    with ``waeup.local.`` prefix (this is also a convention).
+
+    Returns a generator of the found roles.
+    """
+    for name, item in get_all_roles():
+        if not name.startswith('waeup.'):
+            # Ignore non-Kofa roles...
+            continue
+        if not also_local and name.startswith('waeup.local.'):
+            # Ignore local roles...
+            continue
+        yield item
+
+
+def get_waeup_role_names():
+    """Get the ids of all Kofa roles.
+
+    See :func:`get_waeup_roles` for what a 'KofaRole' is.
+
+    This function returns a sorted list of Kofa role names.
+    """
+    return sorted([x.id for x in get_waeup_roles()])
+
+
+class LocalRolesAssignable(grok.Adapter):
+    """Default implementation for `ILocalRolesAssignable`.
+
+    This adapter returns a list for dictionaries for objects for which
+    we want to know the roles assignable to them locally.
+
+    The returned dicts contain a ``name`` and a ``title`` entry which
+    give a role (``name``) and a description, for which kind of users
+    the permission is meant to be used (``title``).
+
+    Having this adapter registered we make sure, that for each normal
+    object we get a valid `ILocalRolesAssignable` adapter.
+
+    Objects that want to offer certain local roles, can do so by
+    setting a (preferably class-) attribute to a list of role ids.
+
+    You can also define different adapters for different contexts to
+    have different role lookup mechanisms become available. But in
+    normal cases it should be sufficient to use this basic adapter.
+    """
+    grok.context(Interface)
+    grok.provides(ILocalRolesAssignable)
+
+    _roles = []
+
+    def __init__(self, context):
+        self.context = context
+        role_ids = getattr(context, 'local_roles', self._roles)
+        self._roles = [(name, role) for name, role in get_all_roles()
+                       if name in role_ids]
+        return
+
+    def __call__(self):
+        """Get a list of dictionaries containing ``names`` (the roles to
+        assign) and ``titles`` (some description of the type of user
+        to assign each role to).
+        """
+        list_of_dict = [dict(
+                name=name,
+                title=role.title,
+                description=role.description)
+                for name, role in self._roles]
+        return sorted(list_of_dict, key=lambda x: x['name'])
+
+
+def get_users_with_local_roles(context):
+    """Get a list of dicts representing the local roles set for `context`.
+
+    Each dict returns `user_name`, `user_title`, `local_role`,
+    `local_role_title`, and `setting` for each entry in the local
+    roles map of the `context` object.
+    """
+    try:
+        role_map = IPrincipalRoleMap(context)
+    except TypeError:
+        # no map no roles.
+        raise StopIteration
+    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+        user = grok.getSite()['users'].get(user_name, None)
+        user_title = getattr(user, 'title', user_name)
+        local_role_title = getattr(
+            dict(get_all_roles()).get(local_role, None), 'title', None)
+        yield dict(user_name=user_name,
+                   user_title=user_title,
+                   local_role=local_role,
+                   local_role_title=local_role_title,
+                   setting=setting)
+
+
+def get_users_with_role(role, context):
+    """Get a list of dicts representing the usres who have been granted
+    a role for `context`.
+    """
+    try:
+        role_map = IPrincipalRoleMap(context)
+    except TypeError:
+        # no map no roles.
+        raise StopIteration
+    for user_name, setting in role_map.getPrincipalsForRole(role):
+        user = grok.getSite()['users'].get(user_name, None)
+        user_title = getattr(user, 'title', user_name)
+        user_email = getattr(user, 'email', None)
+        yield dict(user_name=user_name,
+                   user_title=user_title,
+                   user_email=user_email,
+                   setting=setting)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/refereeentries.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/refereeentries.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/refereeentries.py	(revision 16045)
@@ -0,0 +1,125 @@
+## $Id: schoolgrades.py 12426 2015-01-08 14:25:54Z uli $
+##
+## Copyright (C) 2012 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
+##
+"""Components representing and aggregating referee entries.
+"""
+import grok
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema import Object
+from waeup.kofa.interfaces import IRefereeEntry, IRefereeEntryField
+from waeup.kofa.widgets.objectwidget import (
+    KofaObjectWidget, KofaObjectDisplayWidget
+    )
+
+
+#: A unique default value.
+DEFAULT_VALUE = object()
+
+
+class RefereeEntry(grok.Model):
+    """A referee entry contains a name and a email.
+    """
+    grok.implements(IRefereeEntry)
+    name = FieldProperty(IRefereeEntry['name'])
+    email = FieldProperty(IRefereeEntry['email'])
+
+    def __init__(self, name=None, email=None, email_sent=False):
+        super(RefereeEntry, self).__init__()
+        if name is not None:
+            self.name = name
+        if email is not None:
+            self.email = email
+        self.email_sent = email_sent
+        if not email_sent == True:
+            self.email_sent = False
+        return
+
+    def __eq__(self, obj):
+        """Two RefereeEntry objects are equal if their `name` and
+           `email` are equal.
+        """
+        for name in ('name', 'email',):
+            if getattr(self, name) != getattr(obj, name, DEFAULT_VALUE):
+                return False
+        return True
+
+    def __ne__(self, other):
+        """Two RefereeEntries are not equal, if their equality test fails.
+
+        a != b <-> not(a == b). Python doc tell, that __ne__ should
+        also be rovided, whenever __eq__ is implemented.
+        """
+        return not self.__eq__(other)
+
+    def to_string(self):
+        """A string representation that can be used in exports.
+
+        Returned is a unicode string of format
+        ``(u'<NAME>','<EMAIL>',<EMAILSENT>)``.
+        """
+        return unicode((self.name, self.email, self.email_sent))
+
+    @classmethod
+    def from_string(cls, string):
+        """Create new RefereeEntry instance based on `string`.
+
+        The string is expected to be in format as delivered by
+        meth:`to_string`.
+
+        This is a classmethod. This means, you normally will call::
+
+          RefereeEntry.from_string(mystring)
+
+        i.e. use the `RefereeEntry` class, not an instance thereof.
+        """
+        string = string.replace("u''", "None").replace("''", "None")
+        name, email, email_sent = eval(string)
+        return cls(name, email, email_sent)
+
+
+class RefereeEntryField(Object):
+    """A zope.schema-like field for usage in interfaces.
+
+    If you want to define an interface containing referee entries, you
+    can do so like this::
+
+      class IMyInterface(Interface):
+          my_referee_entry = RefereeEntryField()
+
+    Default widgets are registered to render referee entry fields.
+    """
+    grok.implements(IRefereeEntryField)
+
+    def __init__(self, **kw):
+        super(RefereeEntryField, self).__init__(IRefereeEntry, **kw)
+        return
+
+
+# register KofaObjectWidgets as default widgets for IRefereeEntryFields
+@grok.adapter(IRefereeEntryField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def referee_entry_input_widget(obj, req):
+    return KofaObjectWidget(obj, req, RefereeEntry)
+
+
+# register a display widget for IRefereeEntryFields
+@grok.adapter(IRefereeEntryField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def referee_entry_display_widget(obj, req):
+    return KofaObjectDisplayWidget(obj, req, RefereeEntry)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/reports.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/reports.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/reports.py	(revision 16045)
@@ -0,0 +1,411 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Components for report generation.
+"""
+import grok
+import zc.async.interfaces
+from persistent.list import PersistentList
+from zope import schema
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.hooks import setSite
+from zope.interface import implementer
+from zope.interface import Interface, Attribute
+from waeup.kofa.async import AsyncJob
+from waeup.kofa.interfaces import (
+    IJobManager, JOB_STATUS_MAP, IKofaPluggable, IKofaObject)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import now
+
+class IReport(Interface):
+    """A report.
+    """
+    args = Attribute("""The args passed to constructor""")
+
+    kwargs = Attribute("""The keywords passed to constructor""")
+
+    creation_dt = Attribute(
+        """Datetime when a report was created. The datetime should """
+        """reflect the point of time when the data was fetched, """
+        """not when any output was created.""")
+
+    def create_pdf(job_id):
+        """Generate a PDF copy.
+        """
+
+class IReportGenerator(IKofaObject):
+    """A report generator.
+    """
+    title = Attribute("""Human readable description of report type.""")
+
+    def generate(site, args=[], kw={}):
+        """Generate a report.
+
+        `args` and `kw` are the parameters needed to create a specific
+        report (if any).
+        """
+
+class IReportJob(zc.async.interfaces.IJob):
+
+    finished = schema.Bool(
+        title = u'`True` if the job finished.`',
+        default = False,
+        )
+
+    failed = schema.Bool(
+        title = u"`True` iff the job finished and didn't provide a report.",
+        default = None,
+        )
+
+    def __init__(site, generator_name):
+        """Create a report job via generator."""
+
+class IReportJobContainer(Interface):
+    """A component that contains (maybe virtually) report jobs.
+    """
+    def start_report_job(report_generator_name, user_id, args=[], kw={}):
+        """Start asynchronous report job.
+
+        `report_generator_name`
+            is the name of a report generator utility to be used.
+
+        `user_id`
+            is the ID of the user that triggers the report generation.
+
+        `args` and `kw`
+            args and keywords passed to the generators `generate()`
+            method.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started.
+        """
+
+    def get_running_report_jobs(user_id=None):
+        """Get report jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
+        that order. The ``<generator_name>`` is the utility name of the
+        used report generator.
+
+        If `user_id` is ``None``, all running report jobs are returned.
+        """
+
+    def get_report_jobs_status(user_id=None):
+        """Get running/completed report jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <generator title>`` in that order, where ``<status
+        translated>`` and ``<generator title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the report generator used.
+        """
+
+    def delete_report_entry(entry):
+        """Delete the report job denoted by `entry`.
+
+        Removes `entry` from the local `running_report_jobs` list and
+        also removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
+        as created by :meth:`start_report_job` or returned by
+        :meth:`get_running_report_jobs`.
+        """
+
+    def report_entry_from_job_id(job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+
+class IReportsContainer(grok.interfaces.IContainer, IReportJobContainer,
+                        IKofaObject):
+    """A grok container that holds report jobs.
+    """
+
+class HandleReports(grok.Permission):
+    """The HandleReports permission allows to add any kind of report
+    and to view and remove own reports, i.e. reports which were created by
+    the logged-in user.
+    """
+    grok.name('waeup.handleReports')
+
+class ManageReports(grok.Permission):
+    """The ManageReports permission allows to view, add and remove also
+    the reports of other users. It requires the permission to handle reports.
+    """
+    grok.name('waeup.manageReports')
+
+class ReportsOfficer(grok.Role):
+    """The Reports Officer has the permission to view, add and remove
+    **own** reports.
+    """
+    grok.name('waeup.ReportsOfficer')
+    grok.title(u'Reports Officer')
+    grok.permissions('waeup.handleReports')
+
+class ReportsManager(grok.Role):
+    """The Reports Manager has the permission to view, add and remove
+    **all** reports.
+    """
+    grok.name('waeup.ReportsManager')
+    grok.title(u'Reports Manager')
+    grok.permissions('waeup.handleReports', 'waeup.manageReports')
+
+def get_generators():
+    """Get available report generators.
+
+    Returns an iterator of tuples ``<NAME, GENERATOR>`` with ``NAME``
+    being the name under which the respective generator was
+    registered.
+    """
+    for name, util in getUtilitiesFor(IReportGenerator):
+        yield name, util
+    pass
+
+@implementer(IReport)
+class Report(object):
+    """A base for reports.
+    """
+    creation_dt = None
+
+    def __init__(self, args=[], kwargs={}):
+        self.args = args
+        self.kwargs = kwargs
+        self.creation_dt = now()
+
+    def create_pdf(self, job_id):
+        raise NotImplementedError()
+
+@implementer(IReportGenerator)
+class ReportGenerator(object):
+    """A base for report generators.
+    """
+    title = _("Unnamed Report")
+
+    def generate(self, site, args=[], kw={}):
+        result = Report()
+        return result
+
+def report_job(site, generator_name, args=[], kw={}):
+    """Get a generator and perform report creation.
+
+    `site`
+        is the site for which the report should be created.
+    `generator_name`
+        the global utility name under which the desired generator is
+        registered.
+    `args` and `kw`
+        Arguments and keywords to be passed to the `generate()` method of
+        the desired generator. While `args` should be a list, `kw` should
+        be a dictionary.
+    """
+    setSite(site)
+    generator = getUtility(IReportGenerator, name=generator_name)
+    report = generator.generate(site, *args, **kw)
+    return report
+
+@implementer(IReportJob)
+class AsyncReportJob(AsyncJob):
+    """An IJob that creates reports.
+
+    `AsyncReportJob` instances are regular `AsyncJob` instances with a
+    different constructor API. Instead of a callable to execute, you
+    must pass a `site`, some `generator_name`, and additional args and
+    keywords to create a report.
+
+    The real work is done when an instance of this class is put into a
+    queue. See :mod:`waeup.kofa.async` to learn more about
+    asynchronous jobs.
+
+    The `generator_name` must be the name under which an IReportGenerator
+    utility was registered with the ZCA.
+
+    The `site` must be a valid site  or ``None``.
+
+    The result of an `AsyncReportJob` is an IReport object.
+    """
+    def __init__(self, site, generator_name, args=[], kw={}):
+        self._generator_name = generator_name
+        super(AsyncReportJob, self).__init__(
+            report_job, site, generator_name, args=args, kw=kw)
+
+    @property
+    def finished(self):
+        """A job is marked `finished` if it is completed.
+
+        Please note: a finished report job does not neccessarily
+        provide an IReport result. See meth:`failed`.
+        """
+        return self.status == zc.async.interfaces.COMPLETED
+
+    @property
+    def failed(self):
+        """A report job is marked failed iff it is finished and the
+        result does not provide IReport.
+
+        While a job is unfinished, the `failed` status is ``None``.
+
+        Failed jobs normally provide a `traceback` to examine reasons.
+        """
+        if not self.finished:
+            return None
+        if not IReport.providedBy(getattr(self, 'result', None)):
+            return True
+        return False
+
+@implementer(IReportJobContainer)
+class ReportJobContainer(object):
+    """A mix-in that provides functionality for asynchronous report jobs.
+    """
+    running_report_jobs = PersistentList()
+
+    def start_report_job(self, generator_name, user_id, args=[], kw={}):
+        """Start asynchronous export job.
+
+        `generator_name`
+            is the name of a report generator utility to be used.
+
+        `user_id`
+            is the ID of the user that triggers the report generation.
+
+        `args` and `kw`
+            args and keywords passed to the generators `generate()`
+            method.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        Returns the job ID of the job started, `None` if the job could
+        not be started.
+        """
+        site = grok.getSite()
+        manager = getUtility(IJobManager)
+        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
+        job_id = manager.put(job)
+        if job_id is not None:
+            # Make sure that the persisted list is stored in ZODB
+            self.running_report_jobs = PersistentList(self.running_report_jobs)
+            self.running_report_jobs.append((job_id, generator_name, user_id),)
+        return job_id
+
+    def get_running_report_jobs(self, user_id=None):
+        """Get report jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <generator_name>, <user_id>`` in
+        that order. The ``<generator_name>`` is the utility name of the
+        used report generator.
+
+        If `user_id` is ``None``, all running report jobs are returned.
+        """
+        entries = []
+        to_delete = []
+        manager = getUtility(IJobManager)
+        for entry in self.running_report_jobs:
+            if user_id is not None and entry[2] != user_id:
+                continue
+            if manager.get(entry[0]) is None:
+                to_delete.append(entry)
+                continue
+            entries.append(entry)
+        if to_delete:
+            self.running_report_jobs = PersistentList(
+                [x for x in self.running_report_jobs if x not in to_delete])
+        return entries
+
+    def get_report_jobs_status(self, user_id=None):
+        """Get running/completed report jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <generator title>`` in that order, where ``<status
+        translated>`` and ``<generator title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the report generator used.
+        """
+        entries = self.get_running_report_jobs(user_id)
+        result = []
+        manager = getUtility(IJobManager)
+        for entry in entries:
+            job = manager.get(entry[0])
+            status, status_translated = JOB_STATUS_MAP[job.status]
+            generator = getUtility(IReportGenerator, name=entry[1])
+            generator_name = getattr(generator, 'title', 'unnamed')
+            result.append((status, status_translated, generator_name))
+        return result
+
+    def delete_report_entry(self, entry):
+        """Delete the report job denoted by `entry`.
+
+        Removes `entry` from the local `running_report_jobs` list and
+        also removes the regarding job via the local job manager.
+
+        `entry` is a tuple ``(<job id>, <generator name>, <user id>)``
+        as created by :meth:`start_report_job` or returned by
+        :meth:`get_running_report_jobs`.
+        """
+        manager = getUtility(IJobManager)
+        manager.remove(entry[0], self)
+        new_entries = [x for x in self.running_report_jobs
+                       if x != entry]
+        self.running_report_jobs = PersistentList(new_entries)
+        return
+
+    def report_entry_from_job_id(self, job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+        for entry in self.running_report_jobs:
+            if entry[0] == job_id:
+                return entry
+        return None
+
+@implementer(IReportsContainer)
+class ReportsContainer(grok.Container, ReportJobContainer):
+    """A container for reports.
+    """
+
+@implementer(IKofaPluggable)
+class ReportsContainerPlugin(grok.GlobalUtility):
+    """A plugin that updates sites to contain a reports container.
+    """
+
+    grok.name('reports')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        """Add a reports container for `site`.
+
+        If there is such an object already, we install a fresh one.
+        """
+        if site.get('reports', None) is not None:
+            del site['reports']
+            logger.info('Removed reports container for site "%s"' % name)
+        return self.update(site, name, logger)
+
+    def update(self, site, name, logger):
+        """Install a reports container in `site`.
+
+        If one exists already, do nothing.
+        """
+        if site.get('reports', None) is not None:
+            return
+        site['reports'] = ReportsContainer()
+        logger.info('Added reports container for site "%s"' % name)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/__init__.py	(revision 16045)
@@ -0,0 +1,8 @@
+from waeup.kofa.schema.field import *
+
+#__all__ = tuple(name for name in globals() if not name.startswith('_'))
+
+__all__ = ('CustomizableErrorMsg',
+           'TextLineChoice',
+           'FormattedDate',
+           'PhoneNumber')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/field.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/field.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/field.py	(revision 16045)
@@ -0,0 +1,159 @@
+## $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
+##
+"""Special fields.
+"""
+from zope.interface import implements
+from zope.schema import TextLine, Date
+from zope.schema.interfaces import (
+    ITextLine, IBaseVocabulary, ISource, IContextSourceBinder, InvalidValue,)
+from zope.schema.vocabulary import (
+    SimpleVocabulary, getVocabularyRegistry, VocabularyRegistryError)
+from waeup.kofa.schema.interfaces import IFormattedDate, IPhoneNumber
+
+class CustomizableErrorMsg(object):
+    # Still work in progress
+
+    def __init__(self, not_in_vocab=InvalidValue, **kw):
+        self.not_in_vocab = not_in_vocab
+        return
+
+class TextLineChoice(TextLine, CustomizableErrorMsg):
+    """A TextLine field that also accepts sources, vocabs and values.
+
+    You can use this field like a regular zope.schema.TextLine field.
+
+    You can additionally give `source`, `values` or `vocabulary`
+    parameters which will work mainly as with regular
+    zope.schema.Choice fields (hence the name). Different to
+    `Choice` field, these parameters are not mandatory. Leave them
+    out and you have a regular `TextLine` field.
+
+    If you pass a simple source parameter, only those values are
+    allowed which also appear in the source. The source should of
+    course provide string values as suitable for TextLine fields only!
+
+    If, for example you create a TextLineChoice like this:
+
+      name = TextLineChoice(
+        title = u'Some name',
+        values = [u'foo', u'bar']  # unicode allowed only!
+        )
+
+    any formlib form rendering this field will only accept input u'foo'
+    or u'bar'.
+
+    The main advantage of this modified TextLine field is to support
+    contextual sources. That means you can define some
+    IContextSourceBinder component that looks up catalogs or something
+    external else and decide then, whether the entered value of a form
+    is allowed or not.
+
+    The code herein is mainly copied over from
+    zope.schema._field.Choice with slight modifications.
+    """
+    implements(ITextLine)
+
+    def __init__(self, values=None, vocabulary=None, source=None, **kw):
+        if vocabulary is not None:
+            assert (isinstance(vocabulary, basestring)
+                    or IBaseVocabulary.providedBy(vocabulary))
+            assert source is None, (
+                "You cannot specify both source and vocabulary.")
+        elif source is not None:
+            vocabulary = source
+
+        assert values is None or vocabulary is None, (
+               "You cannot specify both values and vocabulary.")
+
+        self.vocabulary = None
+        self.vocabularyName = None
+        if values is not None:
+            self.vocabulary = SimpleVocabulary.fromValues(values)
+        elif isinstance(vocabulary, (unicode, str)):
+            self.vocabularyName = vocabulary
+        elif (ISource.providedBy(vocabulary) or
+              IContextSourceBinder.providedBy(vocabulary)):
+            self.vocabulary = vocabulary
+        super(TextLineChoice, self).__init__(**kw)
+        return
+
+    source = property(lambda self: self.vocabulary)
+
+    def bind(self, object):
+        """See zope.schema._bootstrapinterfaces.IField."""
+        clone = super(TextLineChoice, self).bind(object)
+        # get registered vocabulary if needed:
+        if IContextSourceBinder.providedBy(self.vocabulary):
+            clone.vocabulary = self.vocabulary(object)
+            assert ISource.providedBy(clone.vocabulary)
+        elif clone.vocabulary is None and self.vocabularyName is not None:
+            vr = getVocabularyRegistry()
+            clone.vocabulary = vr.get(object, self.vocabularyName)
+            assert ISource.providedBy(clone.vocabulary)
+        return clone
+
+    def _validate(self, value):
+        """First validate against the regular TextLine rules. Then check any
+        vocabularies/sources.
+        """
+        # Do TextLine validation
+        super(TextLineChoice, self)._validate(value)
+
+        # Check allowed value range (the vocabulary part)
+        vocabulary = self.vocabulary
+        if vocabulary is None and self.vocabularyName is not None:
+            vr = getVocabularyRegistry()
+            try:
+                vocabulary = vr.get(None, self.vocabularyName)
+            except VocabularyRegistryError:
+                raise ValueError("Can't validate value without vocabulary")
+        if vocabulary and value not in vocabulary:
+            raise InvalidValue(value)
+        return
+
+class FormattedDate(Date):
+    """A date field that supports additional formatting attributes.
+
+    Stores extra attributes (see below). To make use of these
+    attributes in forms, you have to provide widgets that read them
+    and use them in their operations, for instance the
+    `waeup.kofa.widgets.datewidget.FormattedDateWidget`.
+
+    Extra attributes are as follows:
+
+    `date_format`
+      additional attribute to describe desired date format. Must be a
+      string that can be fed to strftime/strptime functions. By
+      default `None`.
+
+    `show_year`
+      boolean indicating whether some kind of year selection should
+      be used with this instance. `False` by default.
+    """
+    implements(IFormattedDate)
+    date_format = None
+    show_year = False
+    def __init__(self, date_format=None, show_year=False, *args, **kw):
+        self.date_format = date_format
+        self.show_year = show_year
+        return super(FormattedDate, self).__init__(*args, **kw)
+
+class PhoneNumber(TextLine):
+    """A schema field for phone numbers.
+    """
+    implements(IPhoneNumber)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/interfaces.py	(revision 16045)
@@ -0,0 +1,42 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+from zope import schema
+from zope.schema.interfaces import IDate, ITextLine
+
+class IFormattedDate(IDate):
+    """A formatted date.
+
+    Basically a zope.schema.IDate, but with optional additional
+    attributes. These attributes _can_ be used by widgets to change
+    the way of editing or displaying a date.
+
+    The waeup.kofa.widgets.datewidget.FormattedDateWidget is a widget
+    that supports these additional attributes.
+    """
+    show_year = schema.Bool(
+        title = u'Show year selector when editing this date?',
+        default = False,
+        )
+    date_format = schema.ASCII(
+        title = u'A date format string suitable for use with strftime.',
+        default = '%Y-%m-%d',
+        )
+
+class IPhoneNumber(ITextLine):
+    """A phone number.
+    """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_fields.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_fields.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_fields.py	(revision 16045)
@@ -0,0 +1,42 @@
+"""Tests for waeup.kofa.schema fields.
+"""
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from zope.schema.interfaces import IDate
+from waeup.kofa.schema import FormattedDate, PhoneNumber
+from waeup.kofa.schema.interfaces import IFormattedDate, IPhoneNumber
+
+class FormattedDateTests(unittest.TestCase):
+    # Tests for FormattedDate field.
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = FormattedDate()
+        verifyClass(IDate, FormattedDate)
+        verifyClass(IFormattedDate, FormattedDate)
+        verifyObject(IDate, obj)
+        verifyObject(IFormattedDate, obj)
+        return
+
+    def test_defaults(self):
+        # we get expected default values for dates.
+        obj = FormattedDate()
+        self.assertEqual(obj.show_year, False)
+        self.assertEqual(obj.date_format, None)
+        return
+
+    def test_attribs(self):
+        # we can set the promised attributes.
+        obj = FormattedDate(show_year=True, date_format='%d.%m.%Y')
+        self.assertEqual(obj.show_year, True)
+        self.assertEqual(obj.date_format, '%d.%m.%Y')
+        return
+
+class PhoneNumberTests(unittest.TestCase):
+    # Tests for PhoneNumber field
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = PhoneNumber()
+        verifyClass(IPhoneNumber, PhoneNumber)
+        verifyObject(IPhoneNumber, obj)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_textlinechoice.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_textlinechoice.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schema/tests/test_textlinechoice.py	(revision 16045)
@@ -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
+##
+"""Test TextLineChoice
+"""
+import unittest
+from zope.interface import implements, directlyProvides
+from zope.schema import vocabulary
+from zope.schema.interfaces import (
+    InvalidValue, ValidationError,
+    IVocabulary, ISource, IContextSourceBinder,)
+from zope.schema.tests.test_strfield import TextLineTest
+from waeup.kofa.schema import TextLineChoice
+
+# Define some helper functions, classes, vars for tests
+class DummyRegistry(vocabulary.VocabularyRegistry):
+    def get(self, object, name):
+        v = SampleVocabulary()
+        v.object = object
+        v.name = name
+        return v
+
+class SampleTerm(object):
+    pass
+
+SAMPLE_VALUES = [u'a', u'b', u'c', u'd']
+class SampleVocabulary(object):
+    implements(IVocabulary)
+
+    def __iter__(self):
+        return iter([self.getTerm(x) for x in SAMPLE_VALUES])
+
+    def __contains__(self, value):
+        return value in SAMPLE_VALUES
+        return 0 <= value < 10
+
+    def __len__(self):
+        return len(SAMPLE_VALUES)
+
+    def getTerm(self, value):
+        if value in self:
+            t = SampleTerm()
+            t.value = value
+            t.upper = value.upper() #2 * value
+            return t
+        raise LookupError("no such value: %r" % value)
+
+class LetterASource(object):
+    # A source that contains all strings with letter 'a'
+    implements(ISource)
+    required_letter = 'a'
+    def __contains__(self, value):
+        return self.required_letter in value
+letter_a_source = LetterASource()
+
+def letter_source_binder(context):
+    source = LetterASource()
+    source.required_letter = context.req_letter
+    return source
+directlyProvides(letter_source_binder, IContextSourceBinder)
+
+class SampleContext(object):
+    req_letter = 'b'
+
+
+
+class TextLineChoiceAsTextLineTest(TextLineTest):
+    # Tests to guarantee TextLine like behaviour of TextLineChoice
+    # i.e. make sure that a TextLineChoice can everything a TextLine can
+    _Field_Factory = TextLineChoice
+
+class TextLineChoice_Values_Tests(unittest.TestCase):
+    # Tests to guarantee values support
+    def test_create_vocabulary(self):
+        choice = TextLineChoice(values=[1, 3])
+        self.assertEqual([term.value for term in choice.vocabulary], [1, 3])
+
+    def test_validate_string(self):
+        choice = TextLineChoice(values=[u'a', u'c'])
+        choice.validate(u'a')
+        #choice.validate('c')
+        choice.validate(u'c')
+        self.assertRaises(InvalidValue, choice.validate, u'd')
+
+    def test_validate_strings(self):
+        choice = TextLineChoice(values=[u'foo', u'bar'])
+        choice.validate(u'foo')
+        choice.validate(u'bar')
+        choice.validate(u'bar')
+        self.assertRaises(InvalidValue, choice.validate, u'baz')
+
+class TextLineChoice_Vocabulary_Tests(unittest.TestCase):
+    # Tests of the TextLineChoice Field using vocabularies.
+    # Most tests were copied (slightly modified) from zope.schema.tests.
+
+    def setUp(self):
+        vocabulary._clear()
+
+    def tearDown(self):
+        vocabulary._clear()
+
+    def check_preconstructed(self, cls, okval, badval):
+        v = SampleVocabulary()
+        field = cls(vocabulary=v)
+        self.assert_(field.vocabulary is v)
+        self.assert_(field.vocabularyName is None)
+        bound = field.bind(None)
+        self.assert_(bound.vocabulary is v)
+        self.assert_(bound.vocabularyName is None)
+        bound.default = okval
+        self.assertEqual(bound.default, okval)
+        self.assertRaises(ValidationError, setattr, bound, "default", badval)
+
+    def test_preconstructed_vocabulary(self):
+        self.check_preconstructed(TextLineChoice, u'a', u'z')
+
+    def check_constructed(self, cls, okval, badval):
+        vocabulary.setVocabularyRegistry(DummyRegistry())
+        field = cls(vocabulary="vocab")
+        self.assert_(field.vocabulary is None)
+        self.assertEqual(field.vocabularyName, "vocab")
+        o = object()
+        bound = field.bind(o)
+        self.assert_(isinstance(bound.vocabulary, SampleVocabulary))
+        bound.default = okval
+        self.assertEqual(bound.default, okval)
+        self.assertRaises(ValidationError, setattr, bound, "default", badval)
+
+    def test_constructed_vocabulary(self):
+        self.check_constructed(TextLineChoice, u'a', u'z')
+
+    def test_create_vocabulary(self):
+        vocabulary.setVocabularyRegistry(DummyRegistry())
+        field = TextLineChoice(vocabulary="vocab")
+        o = object()
+        bound = field.bind(o)
+        self.assertEqual([term.value for term in bound.vocabulary],
+                         SAMPLE_VALUES)
+
+    def test_undefined_vocabulary(self):
+        choice = TextLineChoice(vocabulary="unknown")
+        self.assertRaises(ValueError, choice.validate, u"value")
+
+class TextLineChoice_Source_Tests(unittest.TestCase):
+    # Tests of the TextLineChoice Field using sources.
+
+    def test_simple_source(self):
+        choice = TextLineChoice(__name__='astring', source=letter_a_source)
+        bound = choice.bind(object())
+        self.assertEqual(bound.vocabulary, letter_a_source)
+        return
+
+    def test_validate_simple_source(self):
+        choice = TextLineChoice(__name__='astring', source=letter_a_source)
+        bound = choice.bind(object())
+        bound.validate(u'string_with_letter_a')
+        self.assertRaises(
+            InvalidValue, bound.validate, u'other_string')
+        return
+
+    def test_contextual_source(self):
+        # make sure we can pass contextual sources
+        choice = TextLineChoice(__name__='astring', source=letter_source_binder)
+        bound = choice.bind(SampleContext()) # requires 'b' instead of 'a'
+        self.assertTrue(isinstance(bound.vocabulary, LetterASource))
+        self.assertTrue(bound.vocabulary is not letter_a_source)
+        return
+
+    def test_validate_contextual_source(self):
+        # make sure the values from context are considered
+        choice = TextLineChoice(__name__='astring', source=letter_source_binder)
+        bound = choice.bind(SampleContext()) # requires 'b' instead of 'a'
+        bound.validate(u'string_with_letter_b')
+        self.assertRaises(
+            InvalidValue, bound.validate, u'other_string') # no 'b'
+        return
+
+def test_suite():
+    # We must define a test suite as we do not want the imported
+    # TextLineTest to be collected by testrunner.
+    return unittest.TestSuite((
+        unittest.makeSuite(TextLineChoiceAsTextLineTest),
+        unittest.makeSuite(TextLineChoice_Values_Tests),
+        unittest.makeSuite(TextLineChoice_Vocabulary_Tests),
+        unittest.makeSuite(TextLineChoice_Source_Tests),
+        ))
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schoolgrades.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schoolgrades.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/schoolgrades.py	(revision 16045)
@@ -0,0 +1,121 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Components representing and aggregating school grades.
+"""
+import grok
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema import Object
+from waeup.kofa.interfaces import IResultEntry, IResultEntryField
+from waeup.kofa.widgets.objectwidget import (
+    KofaObjectWidget, KofaObjectDisplayWidget
+    )
+
+
+#: A unique default value.
+DEFAULT_VALUE = object()
+
+
+class ResultEntry(grok.Model):
+    """A result entry contains a subject and a grade.
+    """
+    grok.implements(IResultEntry)
+    subject = FieldProperty(IResultEntry['subject'])
+    grade = FieldProperty(IResultEntry['grade'])
+
+    def __init__(self, subject=None, grade=None):
+        super(ResultEntry, self).__init__()
+        if subject is not None:
+            self.subject = subject
+        if grade is not None:
+            self.grade = grade
+        return
+
+    def __eq__(self, obj):
+        """Two ResultEntry objects are equal if their `subject` and
+           `grade` are equal.
+        """
+        for name in ('subject', 'grade',):
+            if getattr(self, name) != getattr(obj, name, DEFAULT_VALUE):
+                return False
+        return True
+
+    def __ne__(self, other):
+        """Two ResultEntries are not equal, if their equality test fails.
+
+        a != b <-> not(a == b). Python doc tell, that __ne__ should
+        also be rovided, whenever __eq__ is implemented.
+        """
+        return not self.__eq__(other)
+
+    def to_string(self):
+        """A string representation that can be used in exports.
+
+        Returned is a unicode string of format ``(u'<SUBJ>',u'<GRADE>')``.
+        """
+        return unicode((self.subject, self.grade))
+
+    @classmethod
+    def from_string(cls, string):
+        """Create new ResultEntry instance based on `string`.
+
+        The string is expected to be in format as delivered by
+        meth:`to_string`.
+
+        This is a classmethod. This means, you normally will call::
+
+          ResultEntry.from_string(mystring)
+
+        i.e. use the `ResultEntry` class, not an instance thereof.
+        """
+        string = string.replace("u''", "None")
+        subject, grade = eval(string)
+        return cls(subject, grade)
+
+
+class ResultEntryField(Object):
+    """A zope.schema-like field for usage in interfaces.
+
+    If you want to define an interface containing result entries, you
+    can do so like this::
+
+      class IMyInterface(Interface):
+          my_result_entry = ResultEntryField()
+
+    Default widgets are registered to render result entry fields.
+    """
+    grok.implements(IResultEntryField)
+
+    def __init__(self, **kw):
+        super(ResultEntryField, self).__init__(IResultEntry, **kw)
+        return
+
+
+# register KofaObjectWidgets as default widgets for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def result_entry_input_widget(obj, req):
+    return KofaObjectWidget(obj, req, ResultEntry)
+
+
+# register a display widget for IResultEntryFields
+@grok.adapter(IResultEntryField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def result_entry_display_widget(obj, req):
+    return KofaObjectDisplayWidget(obj, req, ResultEntry)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/scripts.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/scripts.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/scripts.py	(revision 16045)
@@ -0,0 +1,42 @@
+"""
+This is a collection of arbitrary scripts for mainly maintenance purposes.
+
+The scripts are not callable via web interface.
+
+Use them (for instance) like so::
+
+    $ ./bin/kofa-debug
+    >>> from waeup.kofa.scripts import add_field_index
+    >>> add_field_index(root['aaue'], 'things_catalog', 'new_id')
+
+Use at your own risk.
+
+"""
+from zope.catalog.field import FieldIndex
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from waeup.kofa.utils.helpers import reindex_cat
+import transaction
+
+
+def get_catalog(app, catalog_name):
+    return getUtility(ICatalog, name=catalog_name, context=app)
+
+
+def add_field_index(app, catalog_name, field_name):
+    """Add a catalog FieldIndex.
+
+    This can become cumbersome through the web API as _all_ objects stored in
+    ZODB will then be indexed and pulled into memory - might become a problem
+    with large databases. This CLI runs faster. You should make sure, that not
+    much traffic is on the site when running the script.
+    """
+    cat = get_catalog(app, catalog_name)
+    if field_name in cat.keys():
+        # index exists already
+        return False
+    cat[field_name] = FieldIndex(field_name=field_name)
+    transaction.commit()
+    reindex_cat(cat)
+    transaction.commit()
+    return True
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/smtp.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/smtp.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/smtp.py	(revision 16045)
@@ -0,0 +1,208 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+"""
+Email (SMTP) services for Kofa.
+
+Note About Encodings
+--------------------
+
+All functions in this module expect any raw strings passed in to be
+encoded 'utf-8' (if you pass in unicode objects instead, this is not a
+problem).
+
+This is because we cannot easily tell from a raw string (it is in fact
+only a byte stream) what encoding it has. In latin-1 and utf-8, for
+instance, there exist some chars (byte values) that have different
+meanings in both encodings. If we see such a char in a byte stream:
+what is it meant to represent? The respective character from latin1 or
+the one from utf-8?
+
+We therefore interpret all internally used raw strings to be encoded as
+utf-8.
+
+The functions in here nevertheless try hard to produce output (mail
+parts, headers, etc.) encoded in the least complex manner. For
+instance if you pass in some address or mail body that is
+representable (correctly) as ASCII or latin-1, we will turn the text
+into that encoding (given, you passed it in as utf-8) to stay as
+compatible as possible with old mailers that do not understand utf-8.
+
+"""
+import grok
+import logging
+from email.Header import Header
+from email.Utils import formataddr
+from email.mime.text import MIMEText
+from zope.component import getUtility
+from zope.sendmail.interfaces import IMailDelivery
+from waeup.kofa.interfaces import IMailService
+
+
+#: The hardcoded from address. Must not by yahoo.com.
+FROM_ADDRESS = 'no-reply@waeup.org'
+
+
+class DefaultMailService(grok.GlobalUtility):
+    """Returns a :class:`zope.sendmail.IMailDelivery`.
+
+    Searches a site from current request (if applicable) and returns
+    the mail delivery set for this site or a fake mailer that does not
+    really send mail (for testing, evaluating, etc.).
+    """
+    grok.implements(IMailService)
+
+    def __call__(self):
+        name = 'No email service'
+        site = grok.getSite()
+        if site is not None:
+            config = site['configuration']
+            name = getattr(config, 'smtp_mailer', name)
+        return getUtility(IMailDelivery, name=name)
+
+
+class FakeSMTPDelivery(grok.GlobalUtility):
+    """A fake mail delivery for testing etc.
+
+    Instead of sending real mails, this mailer only logs received
+    messages to the ``test.smtp`` logger.
+    """
+    grok.implements(IMailDelivery)
+    grok.name('No email service')
+
+    def send(self, fromaddr, toaddrs, message):
+        logger = logging.getLogger('test.smtp')
+        rcpts = ', '.join([x.decode('utf-8') for x in toaddrs])
+        logger.info(
+            u"Sending email from %s to %s:" % (
+                fromaddr.decode('utf-8'), rcpts))
+        logger.info(u"Message:")
+        for line in message.split('\n'):
+            logger.info(u"msg: " + line.decode('utf-8'))
+        return 'fake-message-id@example.com'
+
+CHARSETS = ('US-ASCII', 'ISO-8859-1', 'UTF-8')
+
+
+def encode_header_item(item):
+    """Turns `item`, a string into an SMTP header part string.
+
+    Encodings are checked carefully (we try to encode as ASCII,
+    Latin-1 and UTF-8 in that order).
+
+    If `item` is not a basestring, `None` is returned.
+    """
+    if not isinstance(item, basestring):
+        return None
+    if not isinstance(item, unicode):
+        item = unicode(item, 'utf-8')
+    return str(Header(item, 'iso-8859-1'))  # try ascii, then latin1, utf-8
+
+
+def encode_address(addr, name=u''):
+    """Turn email address parts into a single valid email string.
+
+    The given email address and the name are turned into a single
+    (byte stream) string, suitable for use with ``To:`` or ``From:``
+    headers in emails.
+
+    Any encodings to a mailer-readable format are performed.
+
+    Preferred input format is unicode, although also raw strings (byte
+    streams) work as long as they are decodable from UTF-8.
+
+    That means: if you pass in non-unicode string, take care to
+    deliver utf-8 encoded strings (or plain ASCII).
+
+    Returns a single (raw) string like "My Name <my@sample.com>".
+    """
+    addr = encode_header_item(addr)
+    name = encode_header_item(name)
+    return formataddr((name, addr))
+
+
+def encode_body(text):
+    """Build MIME message part from text.
+
+    You can pass unicode objects or simple strings as text.
+
+    If the input is a simple string, this string is expected
+    to be encoded 'utf-8'!
+
+    Returns a MIMEText object.
+    """
+    if not isinstance(text, unicode):
+        text = unicode(text, 'utf-8')
+    charset = CHARSETS[-1]  # fallback
+    for charset in CHARSETS:
+        try:
+            text = text.encode(charset)
+        except UnicodeError:
+            pass  # try next encoding
+        else:
+            break
+    return MIMEText(text, 'plain', charset)
+
+
+def send_mail(from_name, from_addr, rcpt_name, rcpt_addrs,
+              subject, body, config=None, cc=False):
+    """Send mail.
+
+    Use `IMailService` to send a mail with the parameters
+    delivered.
+
+    Please note: the `from_addr` given will be used for the reply-to
+    (and cc) field only. It will _not_ be used for the `from` field,
+    as yahoo does not allow non-yahoo servers to deliver mail with
+    ``@yahoo.com`` in the from-field.
+
+    The from-address set here will be: `FROM_ADDRESS` as set above.
+
+    ``cc`` tells whether we want the from-address to be CCed. This is
+    not the case by default as we easily act as an open relay
+    otherwise.
+
+    XXX: The hard-coded from-address should be changable or be
+         determined smarter by looking up a FQDN or similar.
+
+    """
+    # format message
+    rcpt_addrs = rcpt_addrs.replace(' ', '').split(',')
+    body_to = ''
+    for email in rcpt_addrs:
+        body_to += '%s, ' % encode_address(email, rcpt_name)
+    body = encode_body(body)
+    sender_addr = encode_address(FROM_ADDRESS, from_name)
+    reply_addr = encode_address(from_addr, from_name)
+    body["From"] = sender_addr
+    body["To"] = body_to.strip(', ')
+    if cc:
+        body["Cc"] = reply_addr
+    body["Reply-To"] = reply_addr
+    body["Subject"] = encode_header_item(subject)
+
+    mailer = getUtility(IMailService)
+    try:
+        email_admin = grok.getSite()['configuration'].email_admin
+        if from_addr != email_admin:
+            rcpt_addrs += [from_addr]
+    except TypeError:
+        # In tests we might not have a site object
+        rcpt_addrs += [from_addr]
+        pass
+    result = mailer().send(FROM_ADDRESS, rcpt_addrs, body.as_string())
+    return result
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/sourcefactory.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/sourcefactory.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/sourcefactory.py	(revision 16045)
@@ -0,0 +1,78 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Smarter contextual sources, that can do faster containment checks.
+"""
+import zc.sourcefactory.policies
+from zc.sourcefactory.factories import ContextualSourceFactory
+from zc.sourcefactory.source import FactoredContextualSource
+
+class SmartFactoredContextualSource(FactoredContextualSource):
+    """A contextual source that can be faster.
+
+    Regular ContextualSources from zc.sourcefactory suffer from very
+    expensive and slow containment checks. __contains__ is executed as
+    a lookup over the complete set of possible values.
+
+    If, however, a source could do this lookup faster, it had no
+    chance to do that with regular ContextualSources.
+
+    This source instead looks for a `contains()` method of a
+    contextual source first and if it has one, this method is called
+    with `value` and `context` as arguments.
+
+    If a source does not provide `contains()`, the expensive default
+    lookup method is done instead.
+    """
+    def __contains__(self, value):
+        """Check whether `value` is part of the source.
+
+        If the factored source provides a `contains(value, context)`
+        method, this method is called for faster containment
+        checks. If not, then the getValues() method of a factored
+        source is used to examine each single item contained in the
+        source.
+        """
+        if hasattr(self.factory, 'contains'):
+            return self.factory.contains(self.context, value)
+        # This is potentially expensive!
+        return super(SmartFactoredContextualSource, self).__contains__(
+            value)
+
+class SmartContextualSourceFactory(ContextualSourceFactory):
+    source_class = SmartFactoredContextualSource
+
+class SmartBasicContextualSourceFactory(
+    SmartContextualSourceFactory,
+    zc.sourcefactory.policies.BasicContextualSourcePolicy):
+    """Abstract base implementation for a basic but smart contextual
+    source factory.
+
+    These factories use any `contains(value, context)` method of
+    factored sources to speed up containment checks.
+
+    Instances of this class can be used as replacement for
+    `zc.sourcefactory.BasicContextualSourceFactory`. If your
+    implementation derived from :class:`SmartBasicContextualFactory`
+    additionally provides a smart `contains()` method, you only get
+    benefits from using this class.
+
+    The basic requirements for a working
+    `SmartBasicContextualSourceFactory` are the same as for regular
+    ones: you have to implement a `getValues(context)` method. You can
+    (optionally) implements a `contains(value, context)` method.
+    """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/startup.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/startup.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/startup.py	(revision 16045)
@@ -0,0 +1,105 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""WSGI application factories that additionally set environment vars.
+
+The `grokcore.startup` factories for creating WSGI applications
+currently do not support setting of arbitrary environment vars.
+
+The below factories add this feature.
+
+Environment vars can be set in any ``.ini`` file used at startup. In
+the ``[DEFAULT]`` section set the option ``env_vars`` with key/value
+pairs as value like so::
+
+  [DEFAULT]
+  zope_conf = <path-to-zope.conf>
+  env_vars = KEY1 value1
+             KEY2 value2
+
+This would set the env vars ``KEY1`` and ``KEY2`` to the respective
+values on startup on an instance.
+
+To activate these factories, in the ``setup.py`` of your project use::
+
+  [paste.app_factory]
+  main = waeup.kofa.startup:env_app_factory
+  debug = waeup.kofa.startup:env_debug_app_factory
+
+in the entry points section (replacing the references to respective
+`grokcore.startup` factories.
+
+Info for developers: paster on startup delivers the options from
+``[DEFAULT]`` section in `.ini` file as a dictionary in the
+`global_conf`.
+"""
+import os
+from ConfigParser import RawConfigParser
+from grokcore.startup import application_factory, debug_application_factory
+
+def _set_env_vars(global_conf):
+    """Set vars from `global_conf['env_vars']` in `os.environ`.
+    """
+    env_vars = global_conf.get('env_vars', None)
+    if not env_vars:
+        return
+    for line in env_vars.split('\n'):
+        key, val = [x.strip() for x in line.strip().split(' ', 1)]
+        os.environ[key] = val
+    return
+
+def env_app_factory(global_conf, **local_conf):
+    """A WSGI application factory that sets environment vars.
+
+    This app factory provides applications as expected by ``paster``
+    and useable as ``[paste.app_factory]`` plugin in setup.py.
+
+    It's a replacement for the stock app factory provided by
+    `grokcore.startup`.
+
+    Additionally it supports extrapolation of the DEFAULT var
+    ``env_vars`` in .ini files used to configure paster.
+
+    With this factory you can set enviroment vars (as in `os.environ`)
+    via the ``env_vars`` keyword set in some `.ini` file::
+
+      env_vars = MY_KEY some_value
+
+    would set the environment variable ``MY_KEY`` to the value
+    ``some_value`` before creating the actual app.
+
+    You can also set multiple keys/values at once like this::
+
+      env_vars = MY_KEY1  Some value
+                 Another_key Anoter_value
+
+    Note, that keys may not contain whitespaces while values
+    may. Both, keys and values, are stripped before being set.
+    """
+    _set_env_vars(global_conf)
+    return application_factory(global_conf, **local_conf)
+
+def env_debug_app_factory(global_conf, **local_conf):
+    """A debugger application factory.
+
+    This is a wrapper around the real factory from `grokcore.startup`
+    that does the same additional things as :func:`env_app_factory`:
+    it sets environment vars given in `env_vars` option of a
+    configuring .ini file for paster.
+    """
+    _set_env_vars(global_conf)
+    return debug_application_factory(global_conf, **local_conf)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/__init__.py	(revision 16045)
@@ -0,0 +1,22 @@
+"""This package contains everything regarding students.
+"""
+# Make this a package.
+from waeup.kofa.students.student import (
+    Student, StudentFactory
+    )
+from waeup.kofa.students.container import StudentsContainer
+from waeup.kofa.students.studycourse import StudentStudyCourse
+from waeup.kofa.students.payments import StudentPaymentsContainer
+from waeup.kofa.students.accommodation import StudentAccommodation
+from waeup.kofa.students.dynamicroles import StudentPrincipalRoleManager
+
+
+__all__ = [
+    'Student',
+    'StudentFactory',
+    'StudentsContainer',
+    'StudentStudyCourse',
+    'StudentPaymentsContainer',
+    'StudentAccommodation',
+    'StudentPrincipalRoleManager',
+    ]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/accommodation.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/accommodation.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/accommodation.py	(revision 16045)
@@ -0,0 +1,196 @@
+## $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
+##
+"""
+Student accommodation components.
+"""
+from datetime import datetime
+import grok
+from zope.component import getUtility, queryUtility
+from zope.component.interfaces import IFactory
+from zope.event import notify
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import academic_sessions_vocab, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import (
+    IStudentAccommodation, IStudentNavigation, IBedTicket, IStudentsUtils)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.hostels.hostel import NOT_OCCUPIED
+
+
+class StudentAccommodation(grok.Container):
+    """This is a container for bed tickets.
+    """
+    grok.implements(IStudentAccommodation, IStudentNavigation)
+    grok.provides(IStudentAccommodation)
+
+    def __init__(self):
+        super(StudentAccommodation, self).__init__()
+        return
+
+    def addBedTicket(self, bedticket):
+        """Add a bed ticket object.
+        """
+        if not IBedTicket.providedBy(bedticket):
+            raise TypeError(
+                'StudentAccommodation containers contain only IBedTicket instances')
+        try:
+            self[str(bedticket.booking_session)] = bedticket
+        except KeyError:
+            if 'booking expired' in self[
+                    str(bedticket.booking_session)].bed_coordinates:
+                del self[str(bedticket.booking_session)]
+        self[str(bedticket.booking_session)] = bedticket
+        return
+
+    @property
+    def student(self):
+        return self.__parent__
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.writeLogMessage(view, message)
+
+StudentAccommodation = attrs_to_fields(StudentAccommodation)
+
+class BedTicket(grok.Model):
+    """This is a bed ticket which shows that the student has booked a bed.
+    """
+    grok.implements(IBedTicket, IStudentNavigation)
+    grok.provides(IBedTicket)
+
+    def __init__(self):
+        super(BedTicket, self).__init__()
+        self.booking_date = datetime.utcnow()
+        self.bed = None
+        return
+
+    @property
+    def student(self):
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
+
+    @property
+    def display_coordinates(self):
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.getBedCoordinates(self)
+
+    @property
+    def maint_payment_made(self):
+        try:
+            if len(self.student['payments']):
+                for ticket in self.student['payments'].values():
+                    if ticket.p_category == 'hostel_maintenance' and \
+                        ticket.p_session == self.booking_session and \
+                        ticket.p_state == 'paid':
+                            return True
+        except TypeError: # in unit tests
+            pass
+        return False
+
+    def relocateStudent(self):
+        """Relocate student if student parameters have changed or the bed_type
+        of the bed has changed.
+        """
+        if self.booking_session != grok.getSite()[
+            'hostels'].accommodation_session:
+            return False, _("Previous session bookings can't be changed.")
+        student = self.student
+        students_utils = getUtility(IStudentsUtils)
+        acc_details  = students_utils.getAccommodationDetails(student)
+        if self.bed != None and \
+              'reserved' in self.bed.bed_type:
+            return False, _("Students in reserved beds can't be relocated.")
+        if acc_details['bt'] == self.bed_type and \
+                self.bed != None and \
+                self.bed.bed_type == self.bed_type and \
+                self.bed.__parent__.__parent__:
+            return False, _("Student can't be relocated.")
+        # Search a bed
+        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
+        entries = cat.searchResults(
+            owner=(student.student_id,student.student_id))
+        if len(entries) and self.bed == None:
+            # If booking has been cancelled but other bed space has been
+            # manually allocated after cancellation use this bed
+            new_bed = [entry for entry in entries][0]
+        else:
+            # Search for other available beds
+            entries = cat.searchResults(
+                bed_type=(acc_details['bt'],acc_details['bt']))
+            available_beds = [
+                entry for entry in entries if entry.owner == NOT_OCCUPIED]
+            desired_hostel = self.__parent__.desired_hostel
+            if desired_hostel and desired_hostel != 'no':
+                # Filter desired hostel beds
+                filtered_beds = [bed for bed in available_beds
+                                 if bed.bed_id.startswith(desired_hostel)]
+                available_beds = filtered_beds
+            if available_beds:
+                new_bed = students_utils.selectBed(available_beds)
+                new_bed.bookBed(student.student_id)
+            else:
+                return False, _('There is no free bed in your category ${a}.',
+                                 mapping = {'a':acc_details['bt']})
+        # Release old bed if exists
+        if self.bed != None:
+            self.bed.owner = NOT_OCCUPIED
+            notify(grok.ObjectModifiedEvent(self.bed))
+        # Designate new bed in ticket
+        self.bed_type = acc_details['bt']
+        self.bed = new_bed
+        hall_title = new_bed.__parent__.hostel_name
+        coordinates = new_bed.coordinates[1:]
+        block, room_nr, bed_nr = coordinates
+        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
+            'a':hall_title, 'b':block,
+            'c':room_nr, 'd':bed_nr,
+            'e':new_bed.bed_type})
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.bed_coordinates = translate(
+            bc, 'waeup.kofa',target_language=portal_language)
+        self.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
+        return True, _('Student relocated: ${a}',
+                 mapping = {'a':self.display_coordinates})
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    def getSessionString(self):
+        return academic_sessions_vocab.getTerm(
+            self.booking_session).title
+
+BedTicket = attrs_to_fields(BedTicket, omit=['display_coordinates'])
+
+
+# Bed tickets must be importable. So we might need a factory.
+class BedTicketFactory(grok.GlobalUtility):
+    """A factory for bed tickets.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.BedTicket')
+    title = u"Create a new bed ticket.",
+    description = u"This factory instantiates new bed ticket instances."
+
+    def __call__(self, *args, **kw):
+        return BedTicket()
+
+    def getInterfaces(self):
+        return implementedBy(BedTicket)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/authentication.py	(revision 16045)
@@ -0,0 +1,317 @@
+## $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
+##
+"""
+Authenticate students.
+"""
+import grok
+import time
+from zope.component import getUtility
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth.interfaces import (
+    IAuthenticatorPlugin, ICredentialsPlugin)
+from zope.pluggableauth.plugins.session import (
+    SessionCredentialsPlugin, SessionCredentials)
+from zope.publisher.interfaces.http import IHTTPRequest
+from zope.session.interfaces import ISession
+from waeup.kofa.authentication import (
+    KofaPrincipalInfo, get_principal_role_manager, FailedLoginInfo)
+from waeup.kofa.interfaces import (
+    IAuthPluginUtility, IUserAccount, IPasswordValidator)
+from waeup.kofa.students.interfaces import IStudent
+
+class StudentAccount(grok.Adapter):
+    """An adapter to turn student objects into accounts on-the-fly.
+    """
+    grok.context(IStudent)
+    grok.implements(IUserAccount)
+
+    public_name = None
+
+    @property
+    def name(self):
+        return self.context.student_id
+
+    @property
+    def password(self):
+        return getattr(self.context, 'password', None)
+
+    @property
+    def title(self):
+        return self.context.display_fullname
+
+    @property
+    def email(self):
+        return self.context.email
+
+    @property
+    def phone(self):
+        return self.context.phone
+
+    @property
+    def user_type(self):
+        return u'student'
+
+    @property
+    def description(self):
+        return self.title
+
+    def suspended(self):
+        return self.context.suspended
+
+    @property
+    def failed_logins(self):
+        if not hasattr(self.context, 'failed_logins'):
+            self.context.failed_logins = FailedLoginInfo()
+        return self.context.failed_logins
+
+    def _get_roles(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+
+    def _set_roles(self, roles):
+        """Set roles for principal denoted by this account.
+        """
+        prm = get_principal_role_manager()
+        old_roles = self.roles
+        for role in old_roles:
+            # Remove old roles, not to be set now...
+            if role.startswith('waeup.') and role not in roles:
+                prm.unsetRoleForPrincipal(role, self.name)
+        for role in roles:
+            prm.assignRoleToPrincipal(role, self.name)
+        return
+
+    roles = property(_get_roles, _set_roles)
+
+    def setPassword(self, password):
+        """Set a password (LDAP-compatible) SSHA encoded.
+
+        We do not store passwords in plaintext. Encrypted password is
+        stored as unicode string.
+        """
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.context.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        """Check whether the given `password` matches the one stored by
+        students, the temporary password was set by officers or
+        the parents_password was set by parents.
+        We additionally check if student account has been suspended
+        or if the portal is blocked.
+        """
+        try:
+            blocker = grok.getSite()['configuration'].maintmode_enabled_by
+            if blocker:
+                return False
+        except TypeError:  # in unit tests
+            pass
+        if not isinstance(password, basestring):
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        temp_password = self.context.getTempPassword()
+        if temp_password:
+            return passwordmanager.checkPassword(temp_password, password)
+        parents_password = self.context.getParentsPassword()
+        if parents_password:
+            return passwordmanager.checkPassword(parents_password, password)
+        if not getattr(self.context, 'password', None):
+            # unset/empty passwords do never match
+            return False
+        if self.context.suspended == True:
+            return False
+        return passwordmanager.checkPassword(self.context.password, password)
+
+class StudentsAuthenticatorPlugin(grok.GlobalUtility):
+    grok.implements(IAuthenticatorPlugin)
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('students')
+
+    def authenticateCredentials(self, credentials):
+        """Authenticate `credentials`.
+
+        `credentials` is a tuple (login, password).
+
+        We look up students to find out whether a respective student
+        exists, then check the password and return the resulting
+        `PrincipalInfo` or ``None`` if no such student can be found.
+        """
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+        account = self.getAccount(credentials['login'])
+        if account is None:
+            return None
+        if not account.checkPassword(credentials['password']):
+            return None
+        return KofaPrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description,
+                             email=account.email,
+                             phone=account.phone,
+                             public_name=account.public_name,
+                             user_type=account.user_type)
+
+    def principalInfo(self, id):
+        """Get a principal identified by `id`.
+
+        This one is required by IAuthenticatorPlugin but not needed here
+        (see respective docstring in applicants package).
+        """
+        return None
+
+    def getAccount(self, login):
+        """Look up a student identified by `login`. Returns an account.
+
+        Currently, we simply look up the key under which the student
+        is stored in the portal. That means we hit if login name and
+        name under which the student is stored match.
+
+        Returns not a student but an account object adapted from any
+        student found.
+
+        If no such student exists, ``None`` is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        studentscontainer = site.get('students', None)
+        if studentscontainer is None:
+            return None
+        student = studentscontainer.get(login, None)
+        if student is None:
+            return None
+        return IUserAccount(student)
+
+class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
+                                      SessionCredentialsPlugin):
+    """A session credentials plugin that handles the case of a user
+    changing his/her own password.
+
+    When users change their own password they might find themselves
+    logged out on next request.
+
+    To avoid this, we support to use a 'change password' page a bit
+    like a regular login page. That means, on each request we lookup
+    the sent data for a login field called 'student_id' and a
+    password.
+
+    If both exist, this means someone sent new credentials.
+
+    We then look for the old credentials stored in the user session.
+    If the new credentials' login (the student_id) matches the old
+    one's, we set the new credentials in session _but_ we return the
+    old credentials for the authentication plugins to check as for the
+    current request (and for the last time) the old credentials apply.
+
+    No valid credentials are returned by this plugin if one of the
+    follwing circumstances is true
+
+    - the sent request is not a regular IHTTPRequest
+
+    - the credentials to set do not match the old ones
+
+    - no credentials are sent with the request
+
+    - no credentials were set before (i.e. the user has no session
+      with credentials set before)
+
+    - no session exists already
+
+    - password and repeated password do not match
+
+    Therefore it is mandatory to put this plugin in the line of all
+    credentials plugins _before_ other plugins, so that the regular
+    credentials plugins can drop in as a 'fallback'.
+
+    This plugin was designed for students to change their passwords,
+    but might be used to allow password resets for other types of
+    accounts as well.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('student_pw_change')
+
+    loginpagename = 'login'
+    loginfield = 'student_id'
+    passwordfield = 'change_password'
+    repeatfield = 'change_password_repeat'
+
+    def extractCredentials(self, request):
+        if not IHTTPRequest.providedBy(request):
+            return None
+        login = request.get(self.loginfield, None)
+        password = request.get(self.passwordfield, None)
+        password_repeat = request.get(self.repeatfield, None)
+
+        if not login or not password:
+            return None
+
+        validator = getUtility(IPasswordValidator)
+        errors = validator.validate_password(password, password_repeat)
+        if errors:
+            return None
+
+        session = ISession(request)
+        sessionData = session.get(
+            'zope.pluggableauth.browserplugins')
+        if not sessionData:
+            return None
+
+        old_credentials = sessionData.get('credentials', None)
+        if old_credentials is None:
+            # Password changes for already authenticated users only!
+            return None
+        if old_credentials.getLogin() != login:
+            # Special treatment only for users that change their own pw.
+            return None
+        old_credentials = {
+            'login': old_credentials.getLogin(),
+            'password': old_credentials.getPassword()}
+
+        # Set new credentials in session. These will be active on next request
+        new_credentials = SessionCredentials(login, password)
+        sessionData['credentials'] = new_credentials
+
+        # Return old credentials for this one request only
+        return old_credentials
+
+class StudentsAuthenticatorSetup(grok.GlobalUtility):
+    """Register or unregister student authentication for a PAU.
+
+    This piece is called when a new site is created.
+    """
+    grok.implements(IAuthPluginUtility)
+    grok.name('students_auth_setup')
+
+    def register(self, pau):
+        plugins = list(pau.credentialsPlugins)
+        # this plugin must come before the regular credentials plugins
+        plugins.insert(0, 'student_pw_change')
+        pau.credentialsPlugins = tuple(plugins)
+        plugins = list(pau.authenticatorPlugins)
+        plugins.append('students')
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
+
+    def unregister(self, pau):
+        plugins = [x for x in pau.authenticatorPlugins
+                   if x != 'students']
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/batching.py	(revision 16045)
@@ -0,0 +1,1062 @@
+## $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
+##
+"""Batch processing components for student objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+
+Here we define the processors for students specific objects like
+students, studycourses, payment tickets and accommodation tickets.
+"""
+import grok
+import unicodecsv as csv # XXX: csv ops should move to dedicated module.
+from time import time
+from datetime import datetime
+from zope.i18n import translate
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from waeup.kofa.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory, VALIDATED, REGISTERED, IGNORE_MARKER, DELETION_MARKER)
+from waeup.kofa.interfaces import IKofaUtils, DuplicationError
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import (
+    IStudent, IStudentStudyCourse, IStudentStudyCourseTransfer,
+    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
+    IStudentStudyLevel, ICourseTicketImport,
+    IStudentOnlinePayment, IStudentVerdictUpdate)
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.students.workflow import  (
+    IMPORTABLE_STATES, IMPORTABLE_TRANSITIONS,
+    FORBIDDEN_POSTGRAD_TRANS, FORBIDDEN_POSTGRAD_STATES)
+from waeup.kofa.utils.batching import BatchProcessor
+
+class StudentProcessor(BatchProcessor):
+    """The Student Processor imports student base data.
+
+    In create mode no locator is required. If no `student_id` is given,
+    the portal automatically assigns a new student id.
+
+    In update or remove mode the processor uses
+    either the `student_id`, `reg_number` or `matric_number` to localize the 
+    student object, exactly in this order. If `student_id` is given and an
+    object can be found, `reg_number` and `matric_number` will be overwritten
+    by the values provided in the import file. If `student_id` is missing,
+    `reg_number` is used to localize the object and only `matric_number`
+    will be overwritten. `matric_number` is used as locator only if both
+    `student_id` and `reg_number` are missing. `student_id` can't be changed
+    by the batch processor.
+
+    There are two ways to change the workflow state of the student,
+    an unsafe and a safe way. The safe way makes use of workflow transitions.
+    Transitions are only possible between allowed workflow states. Only
+    transitions ensure that the registration workflow is maintained.
+
+    **Always prefer the safe way!**
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studentprocessor'
+    grok.name(util_name)
+
+    name = _('Student Processor')
+    iface = IStudent
+    iface_byregnumber = IStudentUpdateByRegNo
+    iface_bymatricnumber = IStudentUpdateByMatricNo
+
+    factory_name = 'waeup.Student'
+
+    @property
+    def available_fields(self):
+        fields = getFields(self.iface)
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number',
+            'password', 'state', 'transition'] + fields.keys())))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        if 'state' in headerfields and 'transition' in headerfields:
+            raise FatalCSVError(
+                "State and transition can't be imported at the same time!")
+        if not 'reg_number' in headerfields and not 'student_id' \
+            in headerfields and not 'matric_number' in headerfields:
+            raise FatalCSVError(
+                "Need at least columns student_id or reg_number " +
+                "or matric_number for import!")
+        if mode == 'create':
+            for field in self.required_fields:
+                if not field in headerfields:
+                    raise FatalCSVError(
+                        "Need at least columns %s for import!" %
+                        ', '.join(["'%s'" % x for x in self.required_fields]))
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def parentsExist(self, row, site):
+        return 'students' in site.keys()
+
+    def getLocator(self, row):
+        if row.get('student_id',None) not in (None, IGNORE_MARKER):
+            return 'student_id'
+        elif row.get('reg_number',None) not in (None, IGNORE_MARKER):
+            return 'reg_number'
+        elif row.get('matric_number',None) not in (None, IGNORE_MARKER):
+            return 'matric_number'
+        else:
+            return None
+
+    # The entry never exists in create mode.
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def getParent(self, row, site):
+        return site['students']
+
+    def getEntry(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if self.getLocator(row) == 'student_id':
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student
+        elif self.getLocator(row) == 'reg_number':
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]
+        elif self.getLocator(row) == 'matric_number':
+            matric_number = row['matric_number']
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(matric_number=(matric_number, matric_number)))
+            if results:
+                return results[0]
+        return None
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addStudent(obj)
+        # Reset _curr_stud_id if student_id has been imported
+        if self.getLocator(row) == 'student_id':
+            parent._curr_stud_id -= 1
+        # We have to log this if state is provided. If not,
+        # logging is done by the event handler handle_student_added
+        if 'state' in row:
+            parent.logger.info('%s - Student record created' % obj.student_id)
+        return
+
+    def delEntry(self, row, site):
+        student = self.getEntry(row, site)
+        if student is not None:
+            parent = self.getParent(row, site)
+            parent.logger.info('%s - Student removed' % student.student_id)
+            del parent[student.student_id]
+        pass
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        transition = row.get('transition', IGNORE_MARKER)
+        if transition not in (IGNORE_MARKER, ''):
+            allowed_transitions = IWorkflowInfo(obj).getManualTransitionIds()
+            if transition not in allowed_transitions:
+                return 'Transition not allowed.'
+            if transition in FORBIDDEN_POSTGRAD_TRANS and \
+                obj.is_postgrad:
+                return 'Transition not allowed (pg student).'
+        state = row.get('state', IGNORE_MARKER)
+        if state not in (IGNORE_MARKER, ''):
+            if state in FORBIDDEN_POSTGRAD_STATES and \
+                obj.is_postgrad:
+                return 'State not allowed (pg student).'
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = ''
+
+        # Remove student_id from row if empty
+        if 'student_id' in row and row['student_id'] in (None, IGNORE_MARKER):
+            row.pop('student_id')
+
+        # Update password
+        if 'password' in row:
+            passwd = row.get('password', IGNORE_MARKER)
+            if passwd not in ('', IGNORE_MARKER):
+                if passwd.startswith('{SSHA}'):
+                    # already encrypted password
+                    obj.password = passwd
+                elif passwd == DELETION_MARKER:
+                    obj.password = None
+                else:
+                    # not yet encrypted password
+                    IUserAccount(obj).setPassword(passwd)
+                items_changed += ('%s=%s, ' % ('password', passwd))
+            row.pop('password')
+
+        # Update registration state
+        if 'state' in row:
+            state = row.get('state', IGNORE_MARKER)
+            if state not in (IGNORE_MARKER, ''):
+                value = row['state']
+                IWorkflowState(obj).setState(value)
+                msg = _("State '${a}' set", mapping = {'a':value})
+                history = IObjectHistory(obj)
+                history.addMessage(msg)
+                items_changed += ('%s=%s, ' % ('state', state))
+            row.pop('state')
+
+        if 'transition' in row:
+            transition = row.get('transition', IGNORE_MARKER)
+            if transition not in (IGNORE_MARKER, ''):
+                value = row['transition']
+                IWorkflowInfo(obj).fireTransition(value)
+                items_changed += ('%s=%s, ' % ('transition', transition))
+            row.pop('transition')
+
+        # apply other values...
+        items_changed += super(StudentProcessor, self).updateEntry(
+            obj, row, site, filename)
+
+        # Log actions...
+        parent = self.getParent(row, site)
+        if hasattr(obj,'student_id'):
+            # Update mode: the student exists and we can get the student_id.
+            # Create mode: the record contains the student_id
+            parent.logger.info(
+                '%s - %s - %s - updated: %s'
+                % (self.name, filename, obj.student_id, items_changed))
+        else:
+            # Create mode: the student does not yet exist
+            # XXX: It seems that this never happens because student_id
+            # is always set.
+            parent.logger.info(
+                '%s - %s - %s - imported: %s' 
+                % (self.name, filename, obj.student_id, items_changed))
+        notify(grok.ObjectModifiedEvent(obj))
+        return items_changed
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['student_id', 'reg_number', 'matric_number'
+                             ] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+    def checkConversion(self, row, mode='create'):
+        """Validates all values in row.
+        """
+        iface = self.iface
+        if mode in ['update', 'remove']:
+            if self.getLocator(row) == 'reg_number':
+                iface = self.iface_byregnumber
+            elif self.getLocator(row) == 'matric_number':
+                iface = self.iface_bymatricnumber
+        converter = IObjectConverter(iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        # We cannot import both state and transition.
+        if 'transition' in row and 'state' in row:
+            if row['transition'] not in (IGNORE_MARKER, '') and \
+                row['state'] not in (IGNORE_MARKER, ''):
+                errs.append(('workflow','not allowed'))
+                return errs, inv_errs, conv_dict
+        if 'transition' in row:
+            if row['transition'] not in IMPORTABLE_TRANSITIONS:
+                if row['transition'] not in (IGNORE_MARKER, ''):
+                    errs.append(('transition','not allowed'))
+        if 'state' in row:
+            if row['state'] not in IMPORTABLE_STATES:
+                if row['state'] not in (IGNORE_MARKER, ''):
+                    errs.append(('state','not allowed'))
+                else:
+                    # State is an attribute of Student and must not
+                    # be changed if empty.
+                    conv_dict['state'] = IGNORE_MARKER
+        try:
+            # Correct stud_id counter. As the IConverter for students
+            # creates student objects that are not used afterwards, we
+            # have to fix the site-wide student_id counter.
+            site = grok.getSite()
+            students = site['students']
+            students._curr_stud_id -= 1
+        except (KeyError, TypeError, AttributeError):
+                pass
+        return errs, inv_errs, conv_dict
+
+
+class StudentProcessorBase(BatchProcessor):
+    """A base for student subitem processor.
+
+    Helps reducing redundancy.
+    """
+    grok.baseclass()
+
+    # additional available fields
+    # beside 'student_id', 'reg_number' and 'matric_number'
+    additional_fields = []
+
+    # additional required fields (subset of additional_fields)
+    additional_fields_required = []
+
+    @property
+    def available_fields(self):
+        fields = ['student_id','reg_number','matric_number'
+                  ] + self.additional_fields
+        return sorted(list(set(fields + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        if not 'reg_number' in headerfields and not 'student_id' \
+            in headerfields and not 'matric_number' in headerfields:
+            raise FatalCSVError(
+                "Need at least columns student_id " +
+                "or reg_number or matric_number for import!")
+        for name in self.additional_fields_required:
+            if not name in headerfields:
+                raise FatalCSVError(
+                    "Need %s for import!" % name)
+
+        # Check for fields to be ignored...
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def _getStudent(self, row, site):
+        NON_VALUES = ['', IGNORE_MARKER]
+        if not 'students' in site.keys():
+            return None
+        if row.get('student_id', '') not in NON_VALUES:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student
+        elif row.get('reg_number', '') not in NON_VALUES:
+            reg_number = row['reg_number']
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                return results[0]
+        elif row.get('matric_number', '') not in NON_VALUES:
+            matric_number = row['matric_number']
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = list(
+                cat.searchResults(matric_number=(matric_number, matric_number)))
+            if results:
+                return results[0]
+        return None
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def entryExists(self, row, site):
+        return self.getEntry(row, site) is not None
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        return errs, inv_errs, conv_dict
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['student_id', 'reg_number', 'matric_number',
+                             'p_id', 'code', 'level'
+                             ] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+
+class StudentStudyCourseProcessor(StudentProcessorBase):
+    """The Student Study Course Processor imports data which refer
+    to the student's course of study. The study course container data
+    describe the current state of the course of study and it stores the
+    entry conditions, i.e. when the student started the course.
+
+    Most important is the `certificate` attribute which tells us which course 
+    the student is studying. The terms 'study course' and 'course of study'
+    are used synonymously. The 'certificate' is the study programme described
+    in the acadmic section. The study course object stores a referrer to a
+    certificate in the acadmic section.
+
+    When importing a new certificate code, `checkConversion` does not only
+    check whether a certificate with the same code exists, it also
+    proves if `current_level` is inside the level range of the certificate.
+    For example, some study programmes start at level 200. The imported
+    current level must thus be 200 or higher.
+
+    `checkUpdateRequirements` looks up if the imported values match the
+    certificate already stored with the study course object. The imported
+    `current_level` must be in the range of the certificate already
+    stored.
+
+    .. note::
+
+      The processor does only offer an update mode. An 'empty' study course 
+      object is automatically created when the student object is added. So this
+      object always exists. It can neither be added a second time nor
+      be removed.
+
+    Students can be transferred by import. A transfer is initialized if the
+    `entry_mode` value is ``transfer``. In this case `checkConversion` uses a
+    different interface for data validation and `checkUpdateRequirements`
+    ensures that a student can only be transferred twice. The student transfer
+    process is described elsewhere.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studycourseupdater'
+    grok.name(util_name)
+
+    name = _('StudentStudyCourse Processor (update only)')
+    iface = IStudentStudyCourse
+    iface_transfer = IStudentStudyCourseTransfer
+    factory_name = 'waeup.StudentStudyCourse'
+
+    def getParent(self, row, site):
+        return self._getStudent(row, site)
+
+    def getEntry(self, row, site):
+        student = self.getParent(row, site)
+        if student is None:
+            return None
+        return student.get('studycourse')
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        entry_mode = row.get('entry_mode', None)
+        certificate = row.get('certificate', None)
+        current_session = row.get('current_session', None)
+        student = self.getParent(row, site)
+        if entry_mode == 'transfer':
+            # We do not expect any error here since we
+            # checked all constraints in checkConversion and
+            # in checkUpdateRequirements
+            student.transfer(
+                certificate=certificate, current_session=current_session)
+            obj = student['studycourse']
+        items_changed = super(StudentStudyCourseProcessor, self).updateEntry(
+            obj, row, site, filename)
+        student.__parent__.logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, student.student_id, items_changed))
+        # Update the students_catalog
+        notify(grok.ObjectModifiedEvent(student))
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        # We have to use the correct interface. Transfer
+        # updates have different constraints.
+        entry_mode = row.get('entry_mode', None)
+        if entry_mode == 'transfer':
+            converter = IObjectConverter(self.iface_transfer)
+        else:
+            converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+
+        # We have to check if current_level is in range of certificate.
+        if 'certificate' in conv_dict and 'current_level' in conv_dict:
+            cert = conv_dict['certificate']
+            level = conv_dict['current_level']
+            if level < cert.start_level or level > cert.end_level+120:
+                errs.append(('current_level','not in range'))
+        return errs, inv_errs, conv_dict
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        certificate = getattr(obj, 'certificate', None)
+        entry_session = getattr(obj, 'entry_session', None)
+        current_level = row.get('current_level', None)
+        entry_mode = row.get('entry_mode', None)
+        # We have to ensure that the student can be transferred.
+        if entry_mode == 'transfer':
+            if certificate is None or entry_session is None:
+                return 'Former study course record incomplete.'
+            if 'studycourse_1' in obj.__parent__.keys() and \
+                'studycourse_2' in obj.__parent__.keys():
+                return 'Maximum number of transfers exceeded.'
+        if current_level:
+            if current_level == 999 and \
+                obj.__parent__.state in FORBIDDEN_POSTGRAD_STATES:
+                return 'Not a pg student.'
+            cert = row.get('certificate', None)
+            if certificate is None and cert is None:
+                return 'No certificate to check level.'
+            if certificate is not None and cert is None and (
+                current_level < certificate.start_level or \
+                current_level > certificate.end_level+120):
+                return 'current_level not in range.'
+        return None
+
+class StudentStudyLevelProcessor(StudentProcessorBase):
+    """The Student Study Level Processor imports study level data.
+    It overwrites the container attributes but not the content of the container,
+    i.e. the course tickets stored inside the container. There is nothing
+    special about this processor.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studylevelprocessor'
+    grok.name(util_name)
+
+    name = _('StudentStudyLevel Processor')
+    iface = IStudentStudyLevel
+    factory_name = 'waeup.StudentStudyLevel'
+
+    @property
+    def available_fields(self):
+        fields = super(StudentStudyLevelProcessor, self).available_fields
+        fields.remove('total_credits')
+        fields.remove('gpa')
+        return  fields
+
+    def getParent(self, row, site):
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['studycourse']
+
+    def getEntry(self, row, site):
+        studycourse = self.getParent(row, site)
+        if studycourse is None:
+            return None
+        try:
+            entry = studycourse.get(str(row['level']))
+        except KeyError:
+            return None
+        return entry
+
+    def delEntry(self, row, site):
+        studylevel = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if studylevel is not None:
+            student = self._getStudent(row, site)
+            student.__parent__.logger.info('%s - Level removed: %s'
+                % (student.student_id, studylevel.__name__))
+            del parent[studylevel.__name__]
+        return
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(StudentStudyLevelProcessor, self).updateEntry(
+            obj, row, site, filename)
+        student = self.getParent(row, site).__parent__
+        student.__parent__.logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, student.student_id, items_changed))
+        return
+
+    def addEntry(self, obj, row, site):
+        if IGNORE_MARKER == str(row['level']):
+            raise FatalCSVError("level: Invalid value")
+        parent = self.getParent(row, site)
+        parent[str(row['level'])] = obj
+        return
+
+    def checkCreateRequirements(self, parent, row, site):
+        """
+        """
+        if parent.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studylevel is locked.'
+        return None
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
+class CourseTicketProcessor(StudentProcessorBase):
+    """The Course Ticket Processor imports course tickets, the subobjects
+    of student study levels (= course lists).
+
+    An imported course ticket contains a copy of the original course data.
+    During import only a few attributes can be set/overwritten.
+
+    Like all other student data importers, this processor also requires
+    either `student_id`, `reg_number` or `matric_number` to find the student.
+    Then it needs `level` and `code` to localize the course ticket.
+
+    `checkConversion` first searches the courses catalog for the imported
+    `code` and ensures that a course with such a code really exists
+    in the academic section. It furthermore checks if `level_session` in
+    the row corresponds with the session of the parent student
+    study level object. It fails if one of the conditions is not met.
+
+    In create mode `fcode`, `dcode`, `title`, `credits`, `passmark` and
+    `semester` are taken from the course found in the academic section.
+    `fcode` and `dcode` can nevermore be changed, neither via the user interface
+    nor by import. Other values can be overwritten by import.
+
+    `ticket_session` is an additional field which can be used to store the
+    session of the course when it was taken. Usually this information is 
+    redundant because the parent study level object already contains this
+    information, except for the study level zero container which can be used to
+    store 'orphaned' course tickets.
+
+    `checkUpdateRequirements` ensures that the `score` attribute can't
+    be accidentally overwritten by import in update mode. The `score`
+    attribute can be unlocked by setting the boolean field
+    `unlock_score` = 1.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseticketprocessor'
+    grok.name(util_name)
+
+    name = _('CourseTicket Processor')
+    iface = ICourseTicketImport
+    factory_name = 'waeup.CourseTicket'
+
+    additional_fields = ['level', 'code']
+    additional_fields_required = additional_fields
+
+    @property
+    def available_fields(self):
+        fields = [
+            'student_id','reg_number','matric_number',
+            'mandatory', 'score', 'carry_over', 'automatic',
+            'outstanding', 'course_category', 'level_session',
+            'title', 'credits', 'passmark', 'semester', 'ticket_session',
+            'unlock_score'
+            ] + self.additional_fields
+        return sorted(fields)
+
+    def getParent(self, row, site):
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['studycourse'].get(str(row['level']))
+
+    def getEntry(self, row, site):
+        level = self.getParent(row, site)
+        if level is None:
+            return None
+        return level.get(row['code'])
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(CourseTicketProcessor, self).updateEntry(
+            obj, row, site, filename)
+        parent = self.getParent(row, site)
+        student = self.getParent(row, site).__parent__.__parent__
+        student.__parent__.logger.info(
+            '%s - %s - %s - %s - updated: %s'
+            % (self.name, filename, student.student_id, parent.level, items_changed))
+        return
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        catalog = getUtility(ICatalog, name='courses_catalog')
+        entries = list(catalog.searchResults(code=(row['code'],row['code'])))
+        obj.fcode = entries[0].__parent__.__parent__.__parent__.code
+        obj.dcode = entries[0].__parent__.__parent__.code
+        if getattr(obj, 'title', None) is None:
+            obj.title = entries[0].title
+        if getattr(obj, 'credits', None) is None:
+            obj.credits = entries[0].credits
+        if getattr(obj, 'passmark', None) is None:
+            obj.passmark = entries[0].passmark
+        if getattr(obj, 'semester', None) is None:
+            obj.semester = entries[0].semester
+        parent[row['code']] = obj
+        return
+
+    def delEntry(self, row, site):
+        ticket = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if ticket is not None:
+            student = self._getStudent(row, site)
+            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
+                % (student.student_id, parent.level, ticket.code))
+            del parent[ticket.code]
+        return
+
+    def checkCreateRequirements(self, parent, row, site):
+        """
+        """
+        if parent.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        if row.get('score',None) and obj.score and not row.get(
+            'unlock_score',None):
+            return 'Score attribute is locked.'
+        return None
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """
+        """
+        if obj.student.studycourse_locked:
+            return 'Studycourse is locked.'
+        return None
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            CourseTicketProcessor, self).checkConversion(row, mode=mode)
+        if mode == 'remove':
+            return errs, inv_errs, conv_dict
+        # In update and create mode we have to check if course really exists.
+        # This is not done by the converter.
+        catalog = getUtility(ICatalog, name='courses_catalog')
+        entries = catalog.searchResults(code=(row['code'],row['code']))
+        if len(entries) == 0:
+            errs.append(('code','non-existent'))
+            return errs, inv_errs, conv_dict
+        # If level_session is provided in row we have to check if
+        # the parent studylevel exists and if its level_session
+        # attribute corresponds with the expected value in row.
+        level_session = conv_dict.get('level_session', IGNORE_MARKER)
+        if level_session not in (IGNORE_MARKER, None):
+            site = grok.getSite()
+            studylevel = self.getParent(row, site)
+            if studylevel is not None:
+                if studylevel.level_session != level_session:
+                    errs.append(('level_session','does not match %s'
+                        % studylevel.level_session))
+            else:
+                errs.append(('level object','does not exist'))
+        return errs, inv_errs, conv_dict
+
+class StudentOnlinePaymentProcessor(StudentProcessorBase):
+    """The Student Online Payment Processor imports student payment tickets.
+    The tickets are located in the ``payments`` subfolder of the student
+    container. The only additional locator is `p_id`, the object id.
+
+    The `checkConversion` method checks the format of the payment identifier.
+    In create mode it does also ensures that same `p_id` does not exist
+    elsewhere. It must be portal-wide unique.
+
+    When adding a payment ticket, the `addEntry` method checks if the same
+    payment has already been made. It compares `p_category` and `p_session`
+    in the row with the corresponding attributes  of existing payment
+    tickets in state ``paid``. If they match, a `DuplicationError` is raised.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'paymentprocessor'
+    grok.name(util_name)
+
+    name = _('StudentOnlinePayment Processor')
+    iface = IStudentOnlinePayment
+    factory_name = 'waeup.StudentOnlinePayment'
+
+    additional_fields = ['p_id']
+
+    @property
+    def available_fields(self):
+        af = super(
+            StudentOnlinePaymentProcessor, self).available_fields
+        af.remove('display_item')
+        return af
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
+        if mode in ('update', 'remove') and not 'p_id' in headerfields:
+            raise FatalCSVError(
+                "Need p_id for import in update and remove modes!")
+        return True
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def getParent(self, row, site):
+        student = self._getStudent(row, site)
+        if student is None:
+            return None
+        return student['payments']
+
+    def getEntry(self, row, site):
+        payments = self.getParent(row, site)
+        if payments is None:
+            return None
+        p_id = row.get('p_id', None)
+        if p_id in (None, IGNORE_MARKER):
+            return None
+        # We can use the hash symbol at the end of p_id in import files
+        # to avoid annoying automatic number transformation
+        # by Excel or Calc
+        p_id = p_id.strip('#')
+        if len(p_id.split('-')) != 3 and not p_id.startswith('p'):
+            # For data migration from old SRP only
+            p_id = 'p' + p_id[7:] + '0'
+        entry = payments.get(p_id)
+        return entry
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = super(StudentOnlinePaymentProcessor, self).updateEntry(
+            obj, row, site, filename)
+        student = self.getParent(row, site).__parent__
+        student.__parent__.logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, student.student_id, items_changed))
+        return
+
+    def samePaymentMade(self, student, category, p_session):
+        for key in student['payments'].keys():
+            ticket = student['payments'][key]
+            if ticket.p_state == 'paid' and\
+               ticket.p_category == category and \
+               ticket.p_session == p_session and \
+               ticket.p_item != 'Balance':
+                  return True
+        return False
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        student = parent.student
+        p_id = row['p_id'].strip('#')
+        # Requirement added on 19/02/2015: same payment must not exist.
+        if obj.p_item != 'Balance' and self.samePaymentMade(
+            student, obj.p_category, obj.p_session):
+            student.__parent__.logger.info(
+                '%s - %s - previous update cancelled'
+                % (self.name, student.student_id))
+            raise DuplicationError('Same payment has already been made.')
+        if len(p_id.split('-')) != 3 and not p_id.startswith('p'):
+            # For data migration from old SRP
+            obj.p_id = 'p' + p_id[7:] + '0'
+            parent[obj.p_id] = obj
+        else:
+            parent[p_id] = obj
+        return
+
+    def delEntry(self, row, site):
+        payment = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if payment is not None:
+            student = self._getStudent(row, site)
+            student.__parent__.logger.info('%s - Payment ticket removed: %s'
+                % (student.student_id, payment.p_id))
+            del parent[payment.p_id]
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            StudentOnlinePaymentProcessor, self).checkConversion(row, mode=mode)
+
+        # We have to check p_id.
+        p_id = row.get('p_id', None)
+        if mode == 'create' and p_id in (None, IGNORE_MARKER):
+            timestamp = ("%d" % int(time()*10000))[1:]
+            p_id = "p%s" % timestamp
+            conv_dict['p_id'] = p_id
+            return errs, inv_errs, conv_dict
+        elif p_id in (None, IGNORE_MARKER):
+            errs.append(('p_id','missing'))
+            return errs, inv_errs, conv_dict
+        else:
+            p_id = p_id.strip('#')
+            if p_id.startswith('p'):
+                if not len(p_id) == 14:
+                    errs.append(('p_id','invalid length'))
+                    return errs, inv_errs, conv_dict
+            elif len(p_id.split('-')) == 3:
+                # The SRP used either pins as keys ...
+                if len(p_id.split('-')[2]) not in (9, 10):
+                    errs.append(('p_id','invalid pin'))
+                    return errs, inv_errs, conv_dict
+            else:
+                # ... or order_ids.
+                if not len(p_id) == 19:
+                    errs.append(('p_id','invalid format'))
+                    return errs, inv_errs, conv_dict
+        # Requirement added on 24/01/2015: p_id must be portal-wide unique.
+        if mode == 'create':
+            cat = getUtility(ICatalog, name='payments_catalog')
+            results = list(cat.searchResults(p_id=(p_id, p_id)))
+            if len(results) > 0:
+                sids = [IPayer(payment).id for payment in results]
+                sids_string = ''
+                for id in sids:
+                    sids_string += '%s ' % id
+                errs.append(('p_id','p_id exists in %s' % sids_string))
+                return errs, inv_errs, conv_dict
+        return errs, inv_errs, conv_dict
+
+class StudentVerdictProcessor(StudentStudyCourseProcessor):
+    """The Student Verdict Processor inherits from the Student Study
+    Course Processor. It's a pure updater. Import step 2 raises a warning
+    message if a datacenter manager tries to select another mode.
+    But it does more than only overwriting study course attributes.
+
+    The Student Verdict Processor is the only processor which cannot be
+    used for restoring data. Purpose is to announce a verdict at the end of
+    each academic session. The processor does not only import a verdict,
+    it also conditions the student data so that the student can pay for the
+    next session and proceed to the next study level.
+
+    The `checkUpdateRequirements` method ensures that the imported data
+    really correspond to the actual state of the student.
+    `current_level` and `current_session` in the row must be on par
+    with the attributes of the study course object. Thus, the processor
+    does not use these values to overwrite the attributes of the study course
+    but to control that the verdict is really meant for the current session of
+    the student. The verdict is only imported if a corresponding study level
+    object exists and the student is in the right registration state,
+    either ``courses validated`` or ``courses registered``. Course registration
+    can be bypassed by setting `bypass_validation` to ``True``.
+
+    The `updateEntry` method does not only update the current verdict of
+    the student study course, it also updates the matching student study
+    level object. It saves the current verdict as `level_verdict` and sets
+    the `validated_by` and `validation_date` attributes, whereas `validated_by` 
+    is taken from the row of the import file and `validation_date` is set to the
+    actual UTC datetime. Finally, the student is moved to state ``returning``.
+    """
+
+    util_name = 'verdictupdater'
+    grok.name(util_name)
+
+    name = _('Verdict Processor (special processor, update only)')
+    iface = IStudentVerdictUpdate
+    factory_name = 'waeup.StudentStudyCourse'
+
+    additional_fields_required = [
+        'current_level', 'current_session', 'current_verdict']
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the studycourse and the student must fulfill
+        before being updated.
+        """
+        # Check if current_levels correspond
+        if obj.current_level != row['current_level']:
+            return 'Current level does not correspond.'
+        # Check if current_sessions correspond
+        if obj.current_session != row['current_session']:
+            return 'Current session does not correspond.'
+        # Check if new verdict is provided
+        if row['current_verdict'] in (IGNORE_MARKER, ''):
+            return 'No verdict in import file.'
+        # Check if studylevel exists
+        level_string = str(obj.current_level)
+        if obj.get(level_string) is None:
+            return 'Study level object is missing.'
+        # Check if student is in state REGISTERED or VALIDATED
+        if row.get('bypass_validation'):
+            if obj.student.state not in (VALIDATED, REGISTERED):
+                return 'Student in wrong state.'
+        else:
+            if obj.student.state != VALIDATED:
+                return 'Student in wrong state.'
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        # Don't set current_session, current_level
+        vals_to_set = dict((key, val) for key, val in row.items()
+                           if key not in ('current_session','current_level'))
+        super(StudentVerdictProcessor, self).updateEntry(
+            obj, vals_to_set, site, filename)
+        parent = self.getParent(row, site)
+        # Set current_verdict in corresponding studylevel
+        level_string = str(obj.current_level)
+        obj[level_string].level_verdict = row['current_verdict']
+        # Fire transition and set studylevel attributes
+        # depending on student's state
+        if obj.__parent__.state == REGISTERED:
+            validated_by = row.get('validated_by', '')
+            if validated_by in (IGNORE_MARKER, ''):
+                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+                system = translate(_('System'),'waeup.kofa',
+                                  target_language=portal_language)
+                obj[level_string].validated_by = system
+            else:
+                obj[level_string].validated_by = validated_by
+            obj[level_string].validation_date = datetime.utcnow()
+            IWorkflowInfo(obj.__parent__).fireTransition('bypass_validation')
+        else:
+            IWorkflowInfo(obj.__parent__).fireTransition('return')
+        # Update the students_catalog
+        notify(grok.ObjectModifiedEvent(obj.__parent__))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser.py	(revision 16045)
@@ -0,0 +1,4258 @@
+## $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 students and related components.
+"""
+import csv
+import grok
+import pytz
+import sys
+import os
+import textwrap
+from cStringIO import StringIO
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from urllib import urlencode
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from zope.event import notify
+from zope.formlib.textwidgets import BytesDisplayWidget
+from zope.i18n import translate
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.security import checkPermission
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
+from waeup.kofa.accesscodes.workflow import USED
+from waeup.kofa.browser.pdf import ENTRY1_STYLE
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.interfaces import ICaptchaManager
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    NullValidator, jsaction, action, UtilityView)
+from waeup.kofa.browser.pages import (
+    ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
+    LocalRoleAssignmentUtilityView)
+from waeup.kofa.hostels.hostel import NOT_OCCUPIED
+from waeup.kofa.interfaces import (
+    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
+    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
+    academic_sessions_vocab, IDataCenter, DOCLINK)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.mandates.mandate import PasswordMandate, ParentsPasswordMandate
+from waeup.kofa.university.interfaces import (
+    IDepartment, ICertificate, ICourse)
+from waeup.kofa.university.certificate import (
+    VirtualCertificateExportJobContainer)
+from waeup.kofa.university.department import (
+    VirtualDepartmentExportJobContainer)
+from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
+from waeup.kofa.university.facultiescontainer import (
+    VirtualFacultiesExportJobContainer)
+from waeup.kofa.university.course import (
+    VirtualCourseExportJobContainer,)
+from waeup.kofa.university.vocabularies import course_levels
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+from waeup.kofa.utils.helpers import get_current_principal, now
+from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
+    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
+    IStudentStudyCourseTransfer,
+    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
+    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
+    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
+    )
+from waeup.kofa.students.catalog import search, StudentQueryResultItem
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.students.workflow import (
+    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
+    VALIDATED, GRADUATED, TRANSREQ, TRANSVAL, TRANSREL, FORBIDDEN_POSTGRAD_TRANS
+    )
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+
+
+class TicketError(Exception):
+    """A course ticket could not be added
+    """
+    pass
+
+# 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())
+    # Inform catalog if certificate has changed
+    # (applyData does this only for the context)
+    if 'certificate' in changed_fields:
+        notify(grok.ObjectModifiedEvent(view.context.student))
+    fields_string = ' + '.join(changed_fields)
+    view.flash(_('Form has been saved.'))
+    if fields_string:
+        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
+    return
+
+def emit_lock_message(view):
+    """Flash a lock message.
+    """
+    view.flash(_('The requested form is locked (read-only).'), type="warning")
+    view.redirect(view.url(view.context))
+    return
+
+def translated_values(view):
+    """Translate course ticket attribute values to be displayed on
+    studylevel pages.
+    """
+    lang = view.request.cookies.get('kofa.language')
+    for value in view.context.values():
+        # We have to unghostify (according to Tres Seaver) the __dict__
+        # by activating the object, otherwise value_dict will be empty
+        # when calling the first time.
+        value._p_activate()
+        value_dict = dict([i for i in value.__dict__.items()])
+        value_dict['url'] = view.url(value)
+        value_dict['removable_by_student'] = value.removable_by_student
+        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
+            target_language=lang)
+        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
+            target_language=lang)
+        value_dict['outstanding'] = translate(str(value.outstanding), 'zope',
+            target_language=lang)
+        value_dict['automatic'] = translate(str(value.automatic), 'zope',
+            target_language=lang)
+        value_dict['grade'] = value.grade
+        value_dict['weight'] = value.weight
+        value_dict['course_category'] = value.course_category
+        value_dict['total_score'] = value.total_score
+        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
+        value_dict['semester'] = semester_dict[
+            value.semester].replace('mester', 'm.')
+        yield value_dict
+
+def addCourseTicket(view, course=None):
+    students_utils = getUtility(IStudentsUtils)
+    ticket = createObject(u'waeup.CourseTicket')
+    ticket.automatic = False
+    ticket.carry_over = False
+    warning = students_utils.warnCreditsOOR(view.context, course)
+    if warning:
+        view.flash(warning, type="warning")
+        return False
+    warning = students_utils.warnCourseAlreadyPassed(view.context, course)
+    if warning:
+        view.flash(warning, type="warning")
+        return False
+    try:
+        view.context.addCourseTicket(ticket, course)
+    except KeyError:
+        view.flash(_('The ticket exists.'), type="warning")
+        return False
+    except TicketError, error:
+        # Ticket errors are not being raised in the base package.
+        view.flash(error, type="warning")
+        return False
+    view.flash(_('Successfully added ${a}.',
+        mapping = {'a':ticket.code}))
+    view.context.writeLogMessage(
+        view,'added: %s|%s|%s' % (
+        ticket.code, ticket.level, ticket.level_session))
+    return True
+
+def level_dict(studycourse):
+    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+    level_dict = {}
+    studylevelsource = StudyLevelSource().factory
+    for code in studylevelsource.getValues(studycourse):
+        title = translate(studylevelsource.getTitle(studycourse, code),
+            'waeup.kofa', target_language=portal_language)
+        level_dict[code] = title
+    return level_dict
+
+class StudentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the students container.
+    """
+    grok.context(IStudentsContainer)
+    title = _('Students')
+
+    @property
+    def target(self):
+        user = get_current_principal()
+        if getattr(user, 'user_type', None) == 'student':
+            return None
+        return self.viewname
+
+class StudentBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student container.
+    """
+    grok.context(IStudent)
+
+    def title(self):
+        return self.context.display_fullname
+
+class SudyCourseBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student study course.
+    """
+    grok.context(IStudentStudyCourse)
+
+    def title(self):
+        if self.context.is_current:
+            return _('Study Course')
+        else:
+            return _('Previous Study Course')
+
+class PaymentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student payments folder.
+    """
+    grok.context(IStudentPaymentsContainer)
+    title = _('Payments')
+
+class OnlinePaymentBreadcrumb(Breadcrumb):
+    """A breadcrumb for payments.
+    """
+    grok.context(IStudentOnlinePayment)
+
+    @property
+    def title(self):
+        return self.context.p_id
+
+class AccommodationBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student accommodation folder.
+    """
+    grok.context(IStudentAccommodation)
+    title = _('Accommodation')
+
+class BedTicketBreadcrumb(Breadcrumb):
+    """A breadcrumb for bed tickets.
+    """
+    grok.context(IBedTicket)
+
+    @property
+    def title(self):
+        return _('Bed Ticket ${a}',
+            mapping = {'a':self.context.getSessionString()})
+
+class StudyLevelBreadcrumb(Breadcrumb):
+    """A breadcrumb for course lists.
+    """
+    grok.context(IStudentStudyLevel)
+
+    @property
+    def title(self):
+        return self.context.level_title
+
+class StudentsContainerPage(KofaPage):
+    """The standard view for student containers.
+    """
+    grok.context(IStudentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewStudentsContainer')
+    grok.template('containerpage')
+    label = _('Find students')
+    search_button = _('Find student(s)')
+    pnav = 4
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) in (
+            'suspended', TRANSREQ, TRANSVAL, GRADUATED):
+            self.searchtype = form['searchtype']
+            self.searchterm = None
+        elif 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type="warning")
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash(_('Only year dates allowed (e.g. 2011).'),
+                           type="danger")
+                return
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash(_('No student found.'), type="warning")
+        return
+
+class StudentsContainerManagePage(KofaPage):
+    """The manage page for student containers.
+    """
+    grok.context(IStudentsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    grok.template('containermanagepage')
+    pnav = 4
+    label = _('Manage students section')
+    search_button = _('Find student(s)')
+    remove_button = _('Remove selected')
+    doclink = DOCLINK + '/students.html'
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) in (
+            'suspended', TRANSREQ, TRANSVAL, GRADUATED):
+            self.searchtype = form['searchtype']
+            self.searchterm = None
+        elif 'searchterm' in form and form['searchterm']:
+            self.searchterm = form['searchterm']
+            self.searchtype = form['searchtype']
+        elif 'old_searchterm' in form:
+            self.searchterm = form['old_searchterm']
+            self.searchtype = form['old_searchtype']
+        else:
+            if 'search' in form:
+                self.flash(_('Empty search string'), type="warning")
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash(_('Only year dates allowed (e.g. 2011).'),
+                           type="danger")
+                return
+        if not 'entries' in form:
+            self.hitlist = search(query=self.searchterm,
+                searchtype=self.searchtype, view=self)
+            if not self.hitlist:
+                self.flash(_('No student found.'), type="warning")
+            if 'remove' in form:
+                self.flash(_('No item selected.'), type="warning")
+            return
+        entries = form['entries']
+        if isinstance(entries, basestring):
+            entries = [entries]
+        deleted = []
+        for entry in entries:
+            if 'remove' in form:
+                del self.context[entry]
+                deleted.append(entry)
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+        return
+
+class StudentAddFormPage(KofaAddFormPage):
+    """Add-form to add a student.
+    """
+    grok.context(IStudentsContainer)
+    grok.require('waeup.manageStudent')
+    grok.name('addstudent')
+    form_fields = grok.AutoFields(IStudent).select(
+        'firstname', 'middlename', 'lastname', 'reg_number')
+    label = _('Add student')
+    pnav = 4
+
+    @action(_('Create student'), style='primary')
+    def addStudent(self, **data):
+        student = createObject(u'waeup.Student')
+        self.applyData(student, **data)
+        self.context.addStudent(student)
+        self.flash(_('Student record created.'))
+        self.redirect(self.url(self.context[student.student_id], 'index'))
+        return
+
+    @action(_('Create graduated student'), style='primary')
+    def addGraduatedStudent(self, **data):
+        student = createObject(u'waeup.Student')
+        self.applyData(student, **data)
+        self.context.addStudent(student)
+        IWorkflowState(student).setState(GRADUATED)
+        notify(grok.ObjectModifiedEvent(student))
+        history = IObjectHistory(student)
+        history.addMessage("State 'graduated' set")
+        self.flash(_('Graduated student record created.'))
+        self.redirect(self.url(self.context[student.student_id], 'index'))
+        return
+
+class LoginAsStudentStep1(KofaEditFormPage):
+    """ View to temporarily set a student password.
+    """
+    grok.context(IStudent)
+    grok.name('loginasstep1')
+    grok.require('waeup.loginAsStudent')
+    grok.template('loginasstep1')
+    pnav = 4
+
+    def update(self):
+        super(LoginAsStudentStep1, self).update()
+        kofa_utils = getUtility(IKofaUtils)
+        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
+        return
+
+    def label(self):
+        return _(u'Set temporary password for ${a}',
+            mapping = {'a':self.context.display_fullname})
+
+    @action('Set password now', style='primary')
+    def setPassword(self, *args, **data):
+        kofa_utils = getUtility(IKofaUtils)
+        password = kofa_utils.genPassword()
+        self.context.setTempPassword(self.request.principal.id, password)
+        self.context.writeLogMessage(
+            self, 'temp_password generated: %s' % password)
+        args = {'password':password}
+        self.redirect(self.url(self.context) +
+            '/loginasstep2?%s' % urlencode(args))
+        return
+
+class LoginAsStudentStep2(KofaPage):
+    """ View to temporarily login as student with a temporary password.
+    """
+    grok.context(IStudent)
+    grok.name('loginasstep2')
+    grok.require('waeup.Public')
+    grok.template('loginasstep2')
+    login_button = _('Login now')
+    pnav = 4
+
+    def label(self):
+        return _(u'Login as ${a}',
+            mapping = {'a':self.context.student_id})
+
+    def update(self, SUBMIT=None, password=None):
+        self.password = password
+        if SUBMIT is not None:
+            self.flash(_('You successfully logged in as student.'))
+            self.redirect(self.url(self.context))
+        return
+
+class StudentBaseDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display student base data
+    """
+    grok.context(IStudent)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    grok.template('basepage')
+    form_fields = grok.AutoFields(IStudentBase).omit(
+        'password', 'suspended', 'suspended_comment', 'flash_notice')
+    pnav = 4
+
+    @property
+    def label(self):
+        if self.context.suspended:
+            return _('${a}: Base Data (account deactivated)',
+                mapping = {'a':self.context.display_fullname})
+        return  _('${a}: Base Data',
+            mapping = {'a':self.context.display_fullname})
+
+    @property
+    def hasPassword(self):
+        if self.context.password:
+            return _('set')
+        return _('unset')
+
+    def update(self):
+        if self.context.flash_notice:
+            self.flash(self.context.flash_notice, type="warning")
+        super(StudentBaseDisplayFormPage, self).update()
+        return
+
+class StudentBasePDFFormPage(KofaDisplayFormPage):
+    """ Page to display student base data in pdf files.
+    """
+
+    def __init__(self, context, request, omit_fields=()):
+        self.omit_fields = omit_fields
+        super(StudentBasePDFFormPage, self).__init__(context, request)
+
+    @property
+    def form_fields(self):
+        form_fields = grok.AutoFields(IStudentBase)
+        for field in self.omit_fields:
+            form_fields = form_fields.omit(field)
+        return form_fields
+
+class ContactStudentFormPage(ContactAdminFormPage):
+    grok.context(IStudent)
+    grok.name('contactstudent')
+    grok.require('waeup.viewStudent')
+    pnav = 4
+    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
+
+    def update(self, subject=u'', body=u''):
+        super(ContactStudentFormPage, self).update()
+        self.form_fields.get('subject').field.default = subject
+        self.form_fields.get('body').field.default = body
+        return
+
+    def label(self):
+        return _(u'Send message to ${a}',
+            mapping = {'a':self.context.display_fullname})
+
+    @action('Send message now', style='primary')
+    def send(self, *args, **data):
+        try:
+            email = self.request.principal.email
+        except AttributeError:
+            email = self.config.email_admin
+        usertype = getattr(self.request.principal,
+                           'user_type', 'system').title()
+        kofa_utils = getUtility(IKofaUtils)
+        success = kofa_utils.sendContactForm(
+                self.request.principal.title,email,
+                self.context.display_fullname,self.context.email,
+                self.request.principal.id,usertype,
+                self.config.name,
+                data['body'],data['subject'])
+        if success:
+            self.flash(_('Your message has been sent.'))
+        else:
+            self.flash(_('An smtp server error occurred.'), type="danger")
+        return
+
+class ExportPDFAdmissionSlip(UtilityView, grok.View):
+    """Deliver a PDF Admission slip.
+    """
+    grok.context(IStudent)
+    grok.name('admission_slip.pdf')
+    grok.require('waeup.viewStudent')
+    prefix = 'form'
+
+    omit_fields = ('date_of_birth', 'current_level')
+
+    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Admission Letter of'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.display_fullname
+
+    def render(self):
+        students_utils = getUtility(IStudentsUtils)
+        letterhead_path = os.path.join(
+            os.path.dirname(__file__), 'static', 'letterhead_admission.jpg')
+        if not os.path.exists(letterhead_path):
+            letterhead_path = None
+        return students_utils.renderPDFAdmissionLetter(self,
+            self.context.student, omit_fields=self.omit_fields,
+            letterhead_path=letterhead_path)
+
+class StudentBaseManageFormPage(KofaEditFormPage):
+    """ View to manage student base data
+    """
+    grok.context(IStudent)
+    grok.name('manage_base')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(IStudentBase).omit(
+        'student_id', 'adm_code', 'suspended')
+    grok.template('basemanagepage')
+    label = _('Manage base data')
+    pnav = 4
+
+    def update(self):
+        super(StudentBaseManageFormPage, self).update()
+        self.wf_info = IWorkflowInfo(self.context)
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('password', None)
+        password_ctl = form.get('control_password', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors), type="danger")
+                return
+        changed_fields = self.applyData(self.context, **data)
+        # Turn list of lists into single list
+        if changed_fields:
+            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
+        else:
+            changed_fields = []
+        if password:
+            # Now we know that the form has no errors and can set password
+            IUserAccount(self.context).setPassword(password)
+            changed_fields.append('password')
+        fields_string = ' + '.join(changed_fields)
+        self.flash(_('Form has been saved.'))
+        if fields_string:
+            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
+        return
+
+class StudentTriggerTransitionFormPage(KofaEditFormPage):
+    """ View to trigger student workflow transitions
+    """
+    grok.context(IStudent)
+    grok.name('trigtrans')
+    grok.require('waeup.triggerTransition')
+    grok.template('trigtrans')
+    label = _('Trigger registration transition')
+    pnav = 4
+
+    def getTransitions(self):
+        """Return a list of dicts of allowed transition ids and titles.
+
+        Each list entry provides keys ``name`` and ``title`` for
+        internal name and (human readable) title of a single
+        transition.
+        """
+        wf_info = IWorkflowInfo(self.context)
+        allowed_transitions = [t for t in wf_info.getManualTransitions()
+            if not t[0].startswith('pay')]
+        if self.context.is_postgrad and not self.context.is_special_postgrad:
+            allowed_transitions = [t for t in allowed_transitions
+                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
+        return [dict(name='', title=_('No transition'))] +[
+            dict(name=x, title=y) for x, y in allowed_transitions]
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        if 'transition' in form and form['transition']:
+            transition_id = form['transition']
+            wf_info = IWorkflowInfo(self.context)
+            wf_info.fireTransition(transition_id)
+        return
+
+class StudentActivateView(UtilityView, grok.View):
+    """ Activate student account
+    """
+    grok.context(IStudent)
+    grok.name('activate')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        self.context.suspended = False
+        self.context.writeLogMessage(self, 'account activated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Student account activated')
+        self.flash(_('Student account has been activated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentDeactivateView(UtilityView, grok.View):
+    """ Deactivate student account
+    """
+    grok.context(IStudent)
+    grok.name('deactivate')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        self.context.suspended = True
+        self.context.writeLogMessage(self, 'account deactivated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Student account deactivated')
+        self.flash(_('Student account has been deactivated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('view_clearance')
+    grok.require('waeup.viewStudent')
+    pnav = 4
+
+    @property
+    def separators(self):
+        return getUtility(IStudentsUtils).SEPARATORS_DICT
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IPGStudentClearance)
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance)
+        if not getattr(self.context, 'officer_comment'):
+            form_fields = form_fields.omit('officer_comment')
+        else:
+            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
+        return form_fields
+
+    @property
+    def label(self):
+        return _('${a}: Clearance Data',
+            mapping = {'a':self.context.display_fullname})
+
+class ExportPDFClearanceSlip(grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudent)
+    grok.name('clearance_slip.pdf')
+    grok.require('waeup.viewStudent')
+    prefix = 'form'
+    omit_fields = (
+        'suspended', 'phone',
+        'adm_code', 'suspended_comment',
+        'date_of_birth', 'current_level',
+        'flash_notice')
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IPGStudentClearance)
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance)
+        if not getattr(self.context, 'officer_comment'):
+            form_fields = form_fields.omit('officer_comment')
+        return form_fields
+
+    @property
+    def title(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Clearance Data'), 'waeup.kofa',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Clearance Slip of'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.display_fullname
+
+    # XXX: not used in waeup.kofa and thus not tested
+    def _signatures(self):
+        isStudent = getattr(
+            self.request.principal, 'user_type', None) == 'student'
+        if not isStudent and self.context.state in (CLEARED, ):
+            return ([_('Student Signature')],
+                    [_('Clearance Officer Signature')])
+        return
+
+    def _sigsInFooter(self):
+        isStudent = getattr(
+            self.request.principal, 'user_type', None) == 'student'
+        if not isStudent and self.context.state in (CLEARED, ):
+            return (_('Date, Student Signature'),
+                    _('Date, Clearance Officer Signature'),
+                    )
+        return ()
+
+    def render(self):
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(
+            self, 'clearance_slip.pdf',
+            self.context.student, studentview, signatures=self._signatures(),
+            sigs_in_footer=self._sigsInFooter(),
+            omit_fields=self.omit_fields)
+
+class StudentClearanceManageFormPage(KofaEditFormPage):
+    """ Page to manage student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('manage_clearance')
+    grok.require('waeup.manageStudent')
+    grok.template('clearanceeditpage')
+    label = _('Manage clearance data')
+    deletion_warning = _('Are you sure?')
+    pnav = 4
+
+    @property
+    def separators(self):
+        return getUtility(IStudentsUtils).SEPARATORS_DICT
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
+        return form_fields
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudentClearView(UtilityView, grok.View):
+    """ Clear student by clearance officer
+    """
+    grok.context(IStudent)
+    grok.name('clear')
+    grok.require('waeup.clearStudent')
+
+    def update(self):
+        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
+            self.context)
+        if cdm:
+            self.flash(cdm)
+            self.redirect(self.url(self.context,'view_clearance'))
+            return
+        if self.context.state == REQUESTED:
+            IWorkflowInfo(self.context).fireTransition('clear')
+            self.flash(_('Student has been cleared.'))
+        else:
+            self.flash(_('Student is in wrong state.'), type="warning")
+        self.redirect(self.url(self.context,'view_clearance'))
+        return
+
+    def render(self):
+        return
+
+class StudentRejectClearancePage(KofaEditFormPage):
+    """ Reject clearance by clearance officers.
+    """
+    grok.context(IStudent)
+    grok.name('reject_clearance')
+    label = _('Reject clearance')
+    grok.require('waeup.clearStudent')
+    form_fields = grok.AutoFields(
+        IUGStudentClearance).select('officer_comment')
+
+    def update(self):
+        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
+            self.context)
+        if cdm:
+            self.flash(cdm, type="warning")
+            self.redirect(self.url(self.context,'view_clearance'))
+            return
+        return super(StudentRejectClearancePage, self).update()
+
+    @action(_('Save comment and reject clearance now'), style='primary')
+    def reject(self, **data):
+        if self.context.state == CLEARED:
+            IWorkflowInfo(self.context).fireTransition('reset4')
+            message = _('Clearance has been annulled.')
+            self.flash(message, type="warning")
+        elif self.context.state == REQUESTED:
+            IWorkflowInfo(self.context).fireTransition('reset3')
+            message = _('Clearance request has been rejected.')
+            self.flash(message, type="warning")
+        else:
+            self.flash(_('Student is in wrong state.'), type="warning")
+            self.redirect(self.url(self.context,'view_clearance'))
+            return
+        self.applyData(self.context, **data)
+        comment = data['officer_comment']
+        if comment:
+            self.context.writeLogMessage(
+                self, 'comment: %s' % comment.replace('\n', '<br>'))
+            args = {'subject':message, 'body':comment}
+        else:
+            args = {'subject':message,}
+        self.redirect(self.url(self.context) +
+            '/contactstudent?%s' % urlencode(args))
+        return
+
+
+class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display student personal data
+    """
+    grok.context(IStudent)
+    grok.name('view_personal')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentPersonal)
+    form_fields['perm_address'].custom_widget = BytesDisplayWidget
+    form_fields[
+        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Personal Data',
+            mapping = {'a':self.context.display_fullname})
+
+class StudentPersonalManageFormPage(KofaEditFormPage):
+    """ Page to manage personal data
+    """
+    grok.context(IStudent)
+    grok.name('manage_personal')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(IStudentPersonal)
+    form_fields['personal_updated'].for_display = True
+    form_fields[
+        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    label = _('Manage personal data')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudentPersonalEditFormPage(KofaEditFormPage):
+    """ Page to edit personal data
+    """
+    grok.context(IStudent)
+    grok.name('edit_personal')
+    grok.require('waeup.handleStudent')
+    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
+    label = _('Edit personal data')
+    pnav = 4
+
+    @action(_('Save/Confirm'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        self.context.personal_updated = datetime.utcnow()
+        return
+
+class StudyCourseDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display the student study course data
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    grok.template('studycoursepage')
+    pnav = 4
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
+                'previous_verdict')
+        else:
+            form_fields = grok.AutoFields(IStudentStudyCourse)
+        return form_fields
+
+    @property
+    def label(self):
+        if self.context.is_current:
+            return _('${a}: Study Course',
+                mapping = {'a':self.context.__parent__.display_fullname})
+        else:
+            return _('${a}: Previous Study Course',
+                mapping = {'a':self.context.__parent__.display_fullname})
+
+    @property
+    def current_mode(self):
+        if self.context.certificate is not None:
+            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
+            return studymodes_dict[self.context.certificate.study_mode]
+        return
+
+    @property
+    def department(self):
+        try:
+            if self.context.certificate is not None:
+                return self.context.certificate.__parent__.__parent__
+        except AttributeError:
+            # handle_certificate_removed does only clear
+            # studycourses with certificate code 'studycourse' but not
+            # 'studycourse_1' or 'studycourse_2'. These certificates do
+            # still exist but have no parents.
+            pass
+        return
+
+    @property
+    def faculty(self):
+        try:
+            if self.context.certificate is not None:
+                return self.context.certificate.__parent__.__parent__.__parent__
+        except AttributeError:
+            # handle_certificate_removed does only clear
+            # studycourses with certificate code 'studycourse' but not
+            # 'studycourse_1' or 'studycourse_2'. These certificates do
+            # still exist but have no parents.
+            pass
+        return
+
+    @property
+    def prev_studycourses(self):
+        if self.context.is_current:
+            if self.context.__parent__.get('studycourse_2', None) is not None:
+                return (
+                        {'href':self.url(self.context.student) + '/studycourse_1',
+                        'title':_('First Study Course, ')},
+                        {'href':self.url(self.context.student) + '/studycourse_2',
+                        'title':_('Second Study Course')}
+                        )
+            if self.context.__parent__.get('studycourse_1', None) is not None:
+                return (
+                        {'href':self.url(self.context.student) + '/studycourse_1',
+                        'title':_('First Study Course')},
+                        )
+        return
+
+class StudyCourseManageFormPage(KofaEditFormPage):
+    """ Page to edit the student study course data
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    grok.template('studycoursemanagepage')
+    label = _('Manage study course')
+    pnav = 4
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
+    tabthreeactions = [_('Add study level')]
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
+                'previous_verdict')
+        else:
+            form_fields = grok.AutoFields(IStudentStudyCourse)
+        return form_fields
+
+    def update(self):
+        if not self.context.is_current \
+            or self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        super(StudyCourseManageFormPage, self).update()
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        try:
+            msave(self, **data)
+        except ConstraintNotSatisfied:
+            # The selected level might not exist in certificate
+            self.flash(_('Current level not available for certificate.'),
+                       type="warning")
+            return
+        notify(grok.ObjectModifiedEvent(self.context.__parent__))
+        return
+
+    @property
+    def level_dicts(self):
+        studylevelsource = StudyLevelSource().factory
+        for code in studylevelsource.getValues(self.context):
+            title = studylevelsource.getTitle(self.context, code)
+            yield(dict(code=code, title=title))
+
+    @property
+    def session_dicts(self):
+        yield(dict(code='', title='--'))
+        for item in academic_sessions():
+            code = item[1]
+            title = item[0]
+            yield(dict(code=code, title=title))
+
+    @action(_('Add study level'), style='primary')
+    def addStudyLevel(self, **data):
+        level_code = self.request.form.get('addlevel', None)
+        level_session = self.request.form.get('level_session', None)
+        if not level_session and not level_code == '0':
+            self.flash(_('You must select a session for the level.'),
+                       type="warning")
+            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+            return
+        if level_session and level_code == '0':
+            self.flash(_('Level zero must not be assigned a session.'),
+                       type="warning")
+            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+            return
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = int(level_code)
+        if level_code != '0':
+            studylevel.level_session = int(level_session)
+        try:
+            self.context.addStudentStudyLevel(
+                self.context.certificate,studylevel)
+            self.flash(_('Study level has been added.'))
+        except KeyError:
+            self.flash(_('This level exists.'), type="warning")
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @jsaction(_('Remove selected levels'))
+    def delStudyLevels(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No study level selected.'), type="warning")
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            del self.context[id]
+            deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+class StudentTranscriptRequestPage(KofaPage):
+    """ Page to request transcript by student
+    """
+    grok.context(IStudent)
+    grok.name('request_transcript')
+    grok.require('waeup.handleStudent')
+    grok.template('transcriptrequest')
+    label = _('Request transcript')
+    ac_prefix = 'TSC'
+    notice = ''
+    pnav = 4
+    buttonname = _('Submit')
+    with_ac = True
+
+    def update(self, SUBMIT=None):
+        super(StudentTranscriptRequestPage, self).update()
+        if not self.context.state == GRADUATED:
+            self.flash(_("Wrong state"), type="danger")
+            self.redirect(self.url(self.context))
+            return
+        if self.with_ac:
+            self.ac_series = self.request.form.get('ac_series', None)
+            self.ac_number = self.request.form.get('ac_number', None)
+        if getattr(
+            self.context['studycourse'], 'transcript_comment', None) is not None:
+            self.correspondence = self.context[
+                'studycourse'].transcript_comment.replace(
+                    '\n', '<br>')
+        else:
+            self.correspondence = ''
+        if SUBMIT is None:
+            return
+        if self.with_ac:
+            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+            code = get_access_code(pin)
+            if not code:
+                self.flash(_('Activation code is invalid.'), type="warning")
+                return
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'),
+                           type="warning")
+                return
+            # Mark pin as used (this also fires a pin related transition)
+            # and fire transition request_transcript
+            comment = _(u"invalidated")
+            # Here we know that the ac is in state initialized so we do not
+            # expect an exception, but the owner might be different
+            if not invalidate_accesscode(pin, comment, self.context.student_id):
+                self.flash(_('You are not the owner of this access code.'),
+                           type="warning")
+                return
+            self.context.clr_code = pin
+        IWorkflowInfo(self.context).fireTransition('request_transcript')
+        comment = self.request.form.get('comment', '').replace('\r', '')
+        address = self.request.form.get('address', '').replace('\r', '')
+        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+        old_transcript_comment = getattr(
+            self.context['studycourse'], 'transcript_comment', None)
+        if old_transcript_comment == None:
+            old_transcript_comment = ''
+        self.context['studycourse'].transcript_comment = '''On %s %s wrote:
+
+%s
+
+Dispatch Address:
+%s
+
+%s''' % (today, self.request.principal.id, comment, address,
+         old_transcript_comment)
+        self.context.writeLogMessage(
+            self, 'comment: %s' % comment.replace('\n', '<br>'))
+        self.flash(_('Transcript processing has been started.'))
+        self.redirect(self.url(self.context))
+        return
+
+class StudentTranscriptSignView(UtilityView, grok.View):
+    """ View to sign transcript
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('sign_transcript')
+    grok.require('waeup.signTranscript')
+
+    def update(self, SUBMIT=None):
+        if self.context.student.state != TRANSVAL:
+            self.flash(_('Student is in wrong state.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        prev_transcript_signees = getattr(
+            self.context, 'transcript_signees', None)
+        if prev_transcript_signees \
+            and '(%s)' % self.request.principal.id in prev_transcript_signees:
+            self.flash(_('You have already signed this transcript.'),
+                type="warning")
+            self.redirect(self.url(self.context) + '/transcript')
+            return
+        self.flash(_('Transcript signed.'))
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.student.__parent__.logger.info(
+            '%s - %s - Transcript signed'
+            % (ob_class, self.context.student.student_id))
+        self.context.student.history.addMessage('Transcript signed')
+        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+        if prev_transcript_signees == None:
+            prev_transcript_signees = ''
+        self.context.transcript_signees = (
+            u"Electronically signed by %s (%s) on %s\n%s"
+            % (self.request.principal.title, self.request.principal.id, today,
+            prev_transcript_signees))
+        self.redirect(self.url(self.context) + '/transcript')
+        return
+
+    def render(self):
+        return
+
+class StudentTranscriptValidateFormPage(KofaEditFormPage):
+    """ Page to validate transcript
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('validate_transcript')
+    grok.require('waeup.processTranscript')
+    grok.template('transcriptprocess')
+    label = _('Validate transcript')
+    buttonname = _('Save comment and validate transcript')
+    pnav = 4
+
+    @property
+    def remarks(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        levelremarks = ''
+        studylevelsource = StudyLevelSource().factory
+        for studylevel in self.context.values():
+            leveltitle = studylevelsource.getTitle(
+                self.context, studylevel.level)
+            url = self.url(self.context) + '/%s/remark' % studylevel.level
+            button_title = translate(
+                _('Edit'), 'waeup.kofa', target_language=portal_language)
+            levelremarks += (
+                '<tr>'
+                '<td>%s:</td>'
+                '<td>%s</td> '
+                '<td><a class="btn btn-primary btn-xs" href="%s">%s</a></td>'
+                '</tr>'
+                ) % (
+                leveltitle, studylevel.transcript_remark, url, button_title)
+        return levelremarks
+
+    def update(self, SUBMIT=None):
+        super(StudentTranscriptValidateFormPage, self).update()
+        if self.context.student.state != TRANSREQ:
+            self.flash(_('Student is in wrong state.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if getattr(self.context, 'transcript_comment', None) is not None:
+            self.correspondence = self.context.transcript_comment.replace(
+                '\n', '<br>')
+        else:
+            self.correspondence = ''
+        if getattr(self.context, 'transcript_signees', None) is not None:
+            self.signees = self.context.transcript_signees.replace(
+                '\n', '<br><br>')
+        else:
+            self.signees = ''
+        if SUBMIT is None:
+            return
+        # Fire transition
+        IWorkflowInfo(self.context.student).fireTransition('validate_transcript')
+        self.flash(_('Transcript validated.'))
+        comment = self.request.form.get('comment', '').replace('\r', '')
+        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+        old_transcript_comment = getattr(
+            self.context, 'transcript_comment', None)
+        if old_transcript_comment == None:
+            old_transcript_comment = ''
+        self.context.transcript_comment = '''On %s %s wrote:
+
+%s
+
+%s''' % (today, self.request.principal.id, comment,
+         old_transcript_comment)
+        self.context.writeLogMessage(
+            self, 'comment: %s' % comment.replace('\n', '<br>'))
+        self.redirect(self.url(self.context) + '/transcript')
+        return
+
+class StudentTranscriptReleaseFormPage(KofaEditFormPage):
+    """ Page to release transcript
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('release_transcript')
+    grok.require('waeup.processTranscript')
+    grok.template('transcriptprocess')
+    label = _('Release transcript')
+    buttonname = _('Save comment and release transcript')
+    pnav = 4
+
+    @property
+    def remarks(self):
+        levelremarks = ''
+        studylevelsource = StudyLevelSource().factory
+        for studylevel in self.context.values():
+            leveltitle = studylevelsource.getTitle(
+                self.context, studylevel.level)
+            levelremarks += "%s: %s <br><br>" % (
+                leveltitle, studylevel.transcript_remark)
+        return levelremarks
+
+    def update(self, SUBMIT=None):
+        super(StudentTranscriptReleaseFormPage, self).update()
+        if self.context.student.state != TRANSVAL:
+            self.flash(_('Student is in wrong state.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if getattr(self.context, 'transcript_comment', None) is not None:
+            self.correspondence = self.context.transcript_comment.replace(
+                '\n', '<br>')
+        else:
+            self.correspondence = ''
+        if getattr(self.context, 'transcript_signees', None) is not None:
+            self.signees = self.context.transcript_signees.replace(
+                '\n', '<br><br>')
+        else:
+            self.signees = ''
+        if SUBMIT is None:
+            return
+        # Fire transition
+        IWorkflowInfo(self.context.student).fireTransition('release_transcript')
+        self.flash(_('Transcript released and final transcript file saved.'))
+        comment = self.request.form.get('comment', '').replace('\r', '')
+        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
+        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
+        old_transcript_comment = getattr(
+            self.context, 'transcript_comment', None)
+        if old_transcript_comment == None:
+            old_transcript_comment = ''
+        self.context.transcript_comment = '''On %s %s wrote:
+
+%s
+
+%s''' % (today, self.request.principal.id, comment,
+         old_transcript_comment)
+        self.context.writeLogMessage(
+            self, 'comment: %s' % comment.replace('\n', '<br>'))
+        # Produce transcript file
+        self.redirect(self.url(self.context) + '/transcript.pdf')
+        return
+
+class StudyCourseTranscriptPage(KofaDisplayFormPage):
+    """ Page to display the student's transcript.
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('transcript')
+    grok.require('waeup.viewTranscript')
+    grok.template('transcript')
+    pnav = 4
+
+    def update(self):
+        final_slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr='final_transcript')
+        if not self.context.student.transcript_enabled or final_slip:
+            self.flash(_('Forbidden!'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        super(StudyCourseTranscriptPage, self).update()
+        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
+        self.level_dict = level_dict(self.context)
+        self.session_dict = dict([(None, 'None'),] +
+            [(item[1], item[0]) for item in academic_sessions()])
+        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
+        return
+
+    @property
+    def label(self):
+        # Here we know that the cookie has been set
+        return _('${a}: Transcript Data', mapping = {
+            'a':self.context.student.display_fullname})
+
+class ExportPDFTranscriptSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('transcript.pdf')
+    grok.require('waeup.viewTranscript')
+    prefix = 'form'
+    omit_fields = (
+        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
+        'password', 'suspended', 'phone', 'email',
+        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
+
+    def update(self):
+        final_slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr='final_transcript')
+        if not self.context.student.transcript_enabled \
+            or final_slip:
+            self.flash(_('Forbidden!'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        super(ExportPDFTranscriptSlip, self).update()
+        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
+        self.level_dict = level_dict(self.context)
+        self.session_dict = dict([(None, 'None'),] +
+            [(item[1], item[0]) for item in academic_sessions()])
+        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
+        return
+
+    @property
+    def label(self):
+        # Here we know that the cookie has been set
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Academic Transcript'),
+            'waeup.kofa', target_language=portal_language)
+
+    def _sigsInFooter(self):
+        if getattr(
+            self.context.student['studycourse'], 'transcript_signees', None):
+            return ()
+        return (_('CERTIFIED TRUE COPY'),)
+
+    def _signatures(self):
+        return ()
+
+    def _digital_sigs(self):
+        if getattr(
+            self.context.student['studycourse'], 'transcript_signees', None):
+            return self.context.student['studycourse'].transcript_signees 
+        return ()
+
+    def _save_file(self):
+        if self.context.student.state == TRANSREL:
+            return True
+        return False
+
+    def render(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
+        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
+        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
+        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
+        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
+        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+
+        tableheader = [(Code,'code', 2.5),
+                         (Title,'title', 7),
+                         (Term, 'semester', 1.5),
+                         (Cred, 'credits', 1.5),
+                         (Score, 'total_score', 1.5),
+                         (Grade, 'grade', 1.5),
+                         ]
+
+        pdfstream = students_utils.renderPDFTranscript(
+            self, 'transcript.pdf',
+            self.context.student, studentview,
+            omit_fields=self.omit_fields,
+            tableheader=tableheader,
+            signatures=self._signatures(),
+            sigs_in_footer=self._sigsInFooter(),
+            digital_sigs=self._digital_sigs(),
+            save_file=self._save_file(),
+            )
+        if not pdfstream:
+            self.redirect(self.url(self.context.student))
+            return
+        return pdfstream
+
+class StudentTransferFormPage(KofaAddFormPage):
+    """Page to transfer the student.
+    """
+    grok.context(IStudent)
+    grok.name('transfer')
+    grok.require('waeup.manageStudent')
+    label = _('Transfer student')
+    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
+        'entry_mode', 'entry_session')
+    pnav = 4
+
+    @jsaction(_('Transfer'))
+    def transferStudent(self, **data):
+        error = self.context.transfer(**data)
+        if error == -1:
+            self.flash(_('Current level does not match certificate levels.'),
+                       type="warning")
+        elif error == -2:
+            self.flash(_('Former study course record incomplete.'),
+                       type="warning")
+        elif error == -3:
+            self.flash(_('Maximum number of transfers exceeded.'),
+                       type="warning")
+        else:
+            self.flash(_('Successfully transferred.'))
+        return
+
+class RevertTransferFormPage(KofaEditFormPage):
+    """View that reverts the previous transfer.
+    """
+    grok.context(IStudent)
+    grok.name('revert_transfer')
+    grok.require('waeup.manageStudent')
+    grok.template('reverttransfer')
+    label = _('Revert previous transfer')
+
+    def update(self):
+        if not self.context.has_key('studycourse_1'):
+            self.flash(_('No previous transfer.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        return
+
+    @jsaction(_('Revert now'))
+    def transferStudent(self, **data):
+        self.context.revert_transfer()
+        self.flash(_('Previous transfer reverted.'))
+        self.redirect(self.url(self.context, 'studycourse'))
+        return
+
+class StudyLevelDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display student study levels
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
+    form_fields[
+        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    grok.template('studylevelpage')
+    pnav = 4
+
+    def update(self):
+        super(StudyLevelDisplayFormPage, self).update()
+        if self.context.level == 0:
+            self.form_fields = self.form_fields.omit('gpa')
+        return
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
+
+    @property
+    def label(self):
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        level_title = translate(self.context.level_title, 'waeup.kofa',
+            target_language=lang)
+        return _('${a}: ${b}', mapping = {
+            'a':self.context.student.display_fullname,
+            'b':level_title})
+
+class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('course_registration_slip.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
+        'level', 'gpa', 'transcript_remark')
+    form_fields[
+        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    prefix = 'form'
+    omit_fields = (
+        'password', 'suspended', 'phone', 'date_of_birth',
+        'adm_code', 'sex', 'suspended_comment', 'current_level',
+        'flash_notice')
+
+    @property
+    def title(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Level Data'), 'waeup.kofa',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('kofa.language', portal_language)
+        level_title = translate(self.context.level_title, 'waeup.kofa',
+            target_language=lang)
+        return translate(_('Course Registration Slip'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % level_title
+
+    @property
+    def tabletitle(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        tabletitle = []
+        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
+            target_language=portal_language))
+        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
+            target_language=portal_language))
+        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
+            target_language=portal_language))
+        return tabletitle
+
+    def _signatures(self):
+        return ()
+
+    def _sigsInFooter(self):
+        return ()
+
+    def render(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
+        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
+        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
+        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
+        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
+        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
+        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
+        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+
+        tabledata = []
+        tableheader = []
+        for i in range(1,7):
+            tabledata.append(sorted(
+                [value for value in self.context.values() if value.semester == i],
+                key=lambda value: str(value.semester) + value.code))
+            tableheader.append([(Code,'code', 2.5),
+                             (Title,'title', 5),
+                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
+                             (Cred, 'credits', 1.5),
+                             #(Mand, 'mandatory', 1.5),
+                             (Score, 'score', 1.5),
+                             (Grade, 'grade', 1.5),
+                             #('Auto', 'automatic', 1.5)
+                             ])
+        return students_utils.renderPDF(
+            self, 'course_registration_slip.pdf',
+            self.context.student, studentview,
+            tableheader=tableheader,
+            tabledata=tabledata,
+            omit_fields=self.omit_fields,
+            signatures=self._signatures(),
+            sigs_in_footer=self._sigsInFooter(),
+            )
+
+class StudyLevelManageFormPage(KofaEditFormPage):
+    """ Page to edit the student study level data
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    grok.template('studylevelmanagepage')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
+        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
+    pnav = 4
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add course ticket'),
+        _('Remove selected tickets'),_('Cancel')]
+    placeholder = _('Enter valid course code')
+
+    def update(self, ADD=None, course=None):
+        if not self.context.__parent__.is_current \
+            or self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        super(StudyLevelManageFormPage, self).update()
+        if ADD is not None:
+            if not course:
+                self.flash(_('No valid course code entered.'), type="warning")
+                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+                return
+            cat = queryUtility(ICatalog, name='courses_catalog')
+            result = cat.searchResults(code=(course, course))
+            if len(result) != 1:
+                self.flash(_('Course not found.'), type="warning")
+            else:
+                course = list(result)[0]
+                addCourseTicket(self, course)
+            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
+
+    @property
+    def label(self):
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        level_title = translate(self.context.level_title, 'waeup.kofa',
+            target_language=lang)
+        return _('Manage ${a}',
+            mapping = {'a':level_title})
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @jsaction(_('Remove selected tickets'))
+    def delCourseTicket(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No ticket selected.'), type="warning")
+            self.redirect(self.url(self.context, '@@manage')+'#tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            del self.context[id]
+            deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s' % (', '.join(deleted)))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+class StudyLevelRemarkFormPage(KofaEditFormPage):
+    """ Page to edit the student study level transcript remark only
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('remark')
+    grok.require('waeup.processTranscript')
+    grok.template('studylevelremarkpage')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
+    form_fields['level_session'].for_display = True
+    form_fields['level_verdict'].for_display = True
+    form_fields['validation_date'].for_display = True
+    form_fields['validated_by'].for_display = True
+
+    def update(self, ADD=None, course=None):
+        if self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        super(StudyLevelRemarkFormPage, self).update()
+
+    @property
+    def label(self):
+        lang = self.request.cookies.get('kofa.language')
+        level_title = translate(self.context.level_title, 'waeup.kofa',
+            target_language=lang)
+        return _(
+            'Edit transcript remark of level ${a}', mapping = {'a':level_title})
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
+
+    @action(_('Save remark and go and back to transcript validation page'),
+        style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        self.redirect(self.url(self.context.student)
+            + '/studycourse/validate_transcript#tab4')
+        return
+
+class ValidateCoursesView(UtilityView, grok.View):
+    """ Validate course list by course adviser
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('validate_courses')
+    grok.require('waeup.validateStudent')
+
+    def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        if str(self.context.student.current_level) != self.context.__name__:
+            self.flash(_('This is not the student\'s current level.'),
+                       type="danger")
+        elif self.context.student.state == REGISTERED:
+            IWorkflowInfo(self.context.student).fireTransition(
+                'validate_courses')
+            self.flash(_('Course list has been validated.'))
+        else:
+            self.flash(_('Student is in the wrong state.'), type="warning")
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class RejectCoursesView(UtilityView, grok.View):
+    """ Reject course list by course adviser
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('reject_courses')
+    grok.require('waeup.validateStudent')
+
+    def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            self.flash(_('This is not the student\'s current level.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        elif self.context.student.state == VALIDATED:
+            IWorkflowInfo(self.context.student).fireTransition('reset8')
+            message = _('Course list request has been annulled.')
+            self.flash(message)
+        elif self.context.student.state == REGISTERED:
+            IWorkflowInfo(self.context.student).fireTransition('reset7')
+            message = _('Course list has been unregistered.')
+            self.flash(message)
+        else:
+            self.flash(_('Student is in the wrong state.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        args = {'subject':message}
+        self.redirect(self.url(self.context.student) +
+            '/contactstudent?%s' % urlencode(args))
+        return
+
+    def render(self):
+        return
+
+class UnregisterCoursesView(UtilityView, grok.View):
+    """Unregister course list by student
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('unregister_courses')
+    grok.require('waeup.handleStudent')
+
+    def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        try:
+            deadline = grok.getSite()['configuration'][
+                str(self.context.level_session)].coursereg_deadline
+        except (TypeError, KeyError):
+            deadline = None
+        if deadline and deadline < datetime.now(pytz.utc):
+            self.flash(_(
+                "Course registration has ended. "
+                "Unregistration is disabled."), type="warning")
+        elif str(self.context.__parent__.current_level) != self.context.__name__:
+            self.flash(_('This is not your current level.'), type="danger")
+        elif self.context.student.state == REGISTERED:
+            IWorkflowInfo(self.context.student).fireTransition('reset7')
+            message = _('Course list has been unregistered.')
+            self.flash(message)
+        else:
+            self.flash(_('You are in the wrong state.'), type="warning")
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class CourseTicketAddFormPage(KofaAddFormPage):
+    """Add a course ticket.
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('add')
+    grok.require('waeup.manageStudent')
+    label = _('Add course ticket')
+    form_fields = grok.AutoFields(ICourseTicketAdd)
+    pnav = 4
+
+    def update(self):
+        if not self.context.__parent__.is_current \
+            or self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        super(CourseTicketAddFormPage, self).update()
+        return
+
+    @action(_('Add course ticket'), style='primary')
+    def addCourseTicket(self, **data):
+        course = data['course']
+        success = addCourseTicket(self, course)
+        if success:
+            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+class CourseTicketDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display course tickets
+    """
+    grok.context(ICourseTicket)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
+        'ticket_session')
+    grok.template('courseticketpage')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Course Ticket ${b}', mapping = {
+            'a':self.context.student.display_fullname,
+            'b':self.context.code})
+
+class CourseTicketManageFormPage(KofaEditFormPage):
+    """ Page to manage course tickets
+    """
+    grok.context(ICourseTicket)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
+    form_fields['title'].for_display = True
+    form_fields['fcode'].for_display = True
+    form_fields['dcode'].for_display = True
+    form_fields['semester'].for_display = True
+    form_fields['passmark'].for_display = True
+    form_fields['credits'].for_display = True
+    form_fields['mandatory'].for_display = False
+    form_fields['automatic'].for_display = True
+    form_fields['carry_over'].for_display = True
+    form_fields['ticket_session'].for_display = True
+    pnav = 4
+    grok.template('courseticketmanagepage')
+
+    def update(self):
+        if not self.context.__parent__.__parent__.is_current \
+            or self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        super(CourseTicketManageFormPage, self).update()
+        return
+
+    @property
+    def label(self):
+        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
+
+    @action('Save', style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class PaymentsManageFormPage(KofaEditFormPage):
+    """ Page to manage the student payments
+
+    This manage form page is for both students and students officers.
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentPaymentsContainer)
+    grok.template('paymentsmanagepage')
+    pnav = 4
+
+    @property
+    def manage_payments_allowed(self):
+        return checkPermission('waeup.payStudent', self.context)
+
+    def unremovable(self, ticket):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        if not self.manage_payments_allowed:
+            return True
+        return (self.request.principal.user_type == 'student' and ticket.r_code)
+
+    @property
+    def label(self):
+        return _('${a}: Payments',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @jsaction(_('Remove selected tickets'))
+    def delPaymentTicket(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No payment selected.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            # Students are not allowed to remove used payment tickets
+            ticket = self.context.get(id, None)
+            if ticket is not None and not self.unremovable(ticket):
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context))
+        return
+
+    #@action(_('Add online payment ticket'))
+    #def addPaymentTicket(self, **data):
+    #    self.redirect(self.url(self.context, '@@addop'))
+
+class OnlinePaymentAddFormPage(KofaAddFormPage):
+    """ Page to add an online payment ticket
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('addop')
+    grok.template('onlinepaymentaddform')
+    grok.require('waeup.payStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment).select('p_combi')
+    label = _('Add online payment')
+    pnav = 4
+
+    @property
+    def selectable_categories(self):
+        student = self.context.__parent__
+        categories = getUtility(
+            IKofaUtils).selectable_payment_categories(student)
+        return sorted(categories.items(), key=lambda value: value[1])
+
+    @action(_('Create ticket'), style='primary')
+    def createTicket(self, **data):
+        form = self.request.form
+        p_category = form.get('form.p_category', None)
+        p_combi = form.get('form.p_combi', [])
+        if isinstance(form.get('form.p_combi', None), unicode):
+            p_combi = [p_combi,]
+        student = self.context.__parent__
+        # The hostel_application payment category is temporarily used
+        # by Uniben.
+        if p_category in ('bed_allocation', 'hostel_application') and student[
+            'studycourse'].current_session != grok.getSite()[
+            'hostels'].accommodation_session:
+                self.flash(
+                    _('Your current session does not match ' + \
+                    'accommodation session.'), type="danger")
+                return
+        if 'maintenance' in p_category:
+            current_session = str(student['studycourse'].current_session)
+            if not current_session in student['accommodation']:
+                self.flash(_('You have not yet booked accommodation.'),
+                           type="warning")
+                return
+        students_utils = getUtility(IStudentsUtils)
+        error, payment = students_utils.setPaymentDetails(
+            p_category, student, None, None, p_combi)
+        if error is not None:
+            self.flash(error, type="danger")
+            return
+        if p_category == 'transfer':
+            payment.p_item = form['new_programme']
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+class PreviousPaymentAddFormPage(KofaAddFormPage):
+    """ Page to add an online payment ticket for previous sessions.
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('addpp')
+    grok.require('waeup.payStudent')
+    form_fields = grok.AutoFields(IStudentPreviousPayment)
+    label = _('Add previous session online payment')
+    pnav = 4
+
+    def update(self):
+        if self.context.student.before_payment:
+            self.flash(_("No previous payment to be made."), type="warning")
+            self.redirect(self.url(self.context))
+        super(PreviousPaymentAddFormPage, self).update()
+        return
+
+    @action(_('Create ticket'), style='primary')
+    def createTicket(self, **data):
+        p_category = data['p_category']
+        previous_session = data.get('p_session', None)
+        previous_level = data.get('p_level', None)
+        student = self.context.__parent__
+        students_utils = getUtility(IStudentsUtils)
+        error, payment = students_utils.setPaymentDetails(
+            p_category, student, previous_session, previous_level, None)
+        if error is not None:
+            self.flash(error, type="danger")
+            return
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+class BalancePaymentAddFormPage(KofaAddFormPage):
+    """ Page to add an online payment which can balance s previous session
+    payment.
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('addbp')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(IStudentBalancePayment)
+    label = _('Add balance')
+    pnav = 4
+
+    @action(_('Create ticket'), style='primary')
+    def createTicket(self, **data):
+        p_category = data['p_category']
+        balance_session = data.get('balance_session', None)
+        balance_level = data.get('balance_level', None)
+        balance_amount = data.get('balance_amount', None)
+        student = self.context.__parent__
+        students_utils = getUtility(IStudentsUtils)
+        error, payment = students_utils.setBalanceDetails(
+            p_category, student, balance_session,
+            balance_level, balance_amount)
+        if error is not None:
+            self.flash(error, type="danger")
+            return
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
+        'p_item', 'p_combi')
+    form_fields[
+        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Online Payment Ticket ${b}', mapping = {
+            'a':self.context.student.display_fullname,
+            'b':self.context.p_id})
+
+class OnlinePaymentApproveView(UtilityView, grok.View):
+    """ Callback view
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('approve')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        flashtype, msg, log = self.context.approveStudentPayment()
+        if log is not None:
+            # Add log message to students.log
+            self.context.writeLogMessage(self,log)
+            # Add log message to payments.log
+            self.context.logger.info(
+                '%s,%s,%s,%s,%s,,,,,,' % (
+                self.context.student.student_id,
+                self.context.p_id, self.context.p_category,
+                self.context.amount_auth, self.context.r_code))
+        self.flash(msg, type=flashtype)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
+    """ Approval view for students.
+
+    This view is used for browser tests only and
+    must be neutralized on custom pages!
+    """
+    grok.name('fake_approve')
+    grok.require('waeup.payStudent')
+
+class ExportPDFPaymentSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('payment_slip.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
+        'p_item', 'p_combi')
+    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    prefix = 'form'
+    note = None
+    omit_fields = (
+        'password', 'suspended', 'phone', 'date_of_birth',
+        'adm_code', 'sex', 'suspended_comment', 'current_level',
+        'flash_notice')
+
+    @property
+    def title(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Payment Data'), 'waeup.kofa',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Online Payment Slip'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.p_id
+
+    def render(self):
+        #if self.context.p_state != 'paid':
+        #    self.flash('Ticket not yet paid.')
+        #    self.redirect(self.url(self.context))
+        #    return
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(self, 'payment_slip.pdf',
+            self.context.student, studentview, note=self.note,
+            omit_fields=self.omit_fields)
+
+class AccommodationDisplayFormPage(KofaDisplayFormPage):
+    """ Page to view bed tickets.
+    This manage form page is for both students and students officers.
+    """
+    grok.context(IStudentAccommodation)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentAccommodation)
+    grok.template('accommodationpage')
+    pnav = 4
+    with_hostel_selection = True
+
+    @property
+    def label(self):
+        return _('${a}: Accommodation',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @property
+    def desired_hostel(self):
+        if self.context.desired_hostel == 'no':
+            return _('No favoured hostel')
+        if self.context.desired_hostel:
+            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
+            if hostel is not None:
+                return hostel.hostel_name
+        return
+
+    def update(self):
+        if checkPermission('waeup.handleAccommodation', self.context):
+            self.redirect(self.url(self.context, 'manage'))
+
+class AccommodationManageFormPage(KofaEditFormPage):
+    """ Page to manage bed tickets.
+
+    This manage form page is for both students and students officers.
+    """
+    grok.context(IStudentAccommodation)
+    grok.name('manage')
+    grok.require('waeup.handleAccommodation')
+    form_fields = grok.AutoFields(IStudentAccommodation)
+    grok.template('accommodationmanagepage')
+    pnav = 4
+    with_hostel_selection = True
+
+    @property
+    def booking_allowed(self):
+        students_utils = getUtility(IStudentsUtils)
+        acc_details  = students_utils.getAccommodationDetails(self.context.student)
+        error_message = students_utils.checkAccommodationRequirements(
+            self.context.student, acc_details)
+        if error_message:
+            return False
+        return True
+
+    @property
+    def actionsgroup1(self):
+        if not self.booking_allowed:
+            return []
+        if not self.with_hostel_selection:
+            return []
+        return [_('Save')]
+
+    @property
+    def actionsgroup2(self):
+        if getattr(self.request.principal, 'user_type', None) == 'student':
+            ## Book button can be disabled in custom packages by
+            ## uncommenting the following lines.
+            #if not self.booking_allowed:
+            #    return []
+            return [_('Book accommodation')]
+        return [_('Book accommodation'), _('Remove selected')]
+
+    @property
+    def label(self):
+        return _('${a}: Accommodation',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @property
+    def desired_hostel(self):
+        if self.context.desired_hostel == 'no':
+            return _('No favoured hostel')
+        if self.context.desired_hostel:
+            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
+            if hostel is not None:
+                return hostel.hostel_name
+        return
+
+    def getHostels(self):
+        """Get a list of all stored hostels.
+        """
+        yield(dict(name=None, title='--', selected=''))
+        selected = ''
+        if self.context.desired_hostel == 'no':
+          selected = 'selected'
+        yield(dict(name='no', title=_('No favoured hostel'), selected=selected))
+        for val in grok.getSite()['hostels'].values():
+            selected = ''
+            if val.hostel_id == self.context.desired_hostel:
+                selected = 'selected'
+            yield(dict(name=val.hostel_id, title=val.hostel_name,
+                       selected=selected))
+
+    @action(_('Save'), style='primary')
+    def save(self):
+        hostel = self.request.form.get('hostel', None)
+        self.context.desired_hostel = hostel
+        self.flash(_('Your selection has been saved.'))
+        return
+
+    @action(_('Book accommodation'), style='primary')
+    def bookAccommodation(self, **data):
+        self.redirect(self.url(self.context, 'add'))
+        return
+
+    @jsaction(_('Remove selected'))
+    def delBedTickets(self, **data):
+        if getattr(self.request.principal, 'user_type', None) == 'student':
+            self.flash(_('You are not allowed to remove bed tickets.'),
+                       type="warning")
+            self.redirect(self.url(self.context))
+            return
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No bed ticket selected.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            del self.context[id]
+            deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: % s' % ', '.join(deleted))
+        self.redirect(self.url(self.context))
+        return
+
+class BedTicketAddPage(KofaPage):
+    """ Page to add a bed ticket
+    """
+    grok.context(IStudentAccommodation)
+    grok.name('add')
+    grok.require('waeup.handleAccommodation')
+    #grok.template('enterpin')
+    ac_prefix = 'HOS'
+    label = _('Add bed ticket')
+    pnav = 4
+    buttonname = _('Create bed ticket')
+    notice = ''
+    with_ac = True
+    with_bedselection = True
+
+    @property
+    def getAvailableBeds(self):
+        """Get a list of all available beds.
+        """
+        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
+        entries = cat.searchResults(
+            bed_type=(self.acc_details['bt'],self.acc_details['bt']))
+        available_beds = [
+            entry for entry in entries if entry.owner == NOT_OCCUPIED]
+        desired_hostel = self.context.desired_hostel
+        # Filter desired hostel beds
+        if desired_hostel and desired_hostel != 'no':
+            filtered_beds = [bed for bed in available_beds
+                             if bed.bed_id.startswith(desired_hostel)]
+            available_beds = filtered_beds
+        # Add legible bed coordinates
+        for bed in available_beds:
+            hall_title = bed.__parent__.hostel_name
+            coordinates = bed.coordinates[1:]
+            block, room_nr, bed_nr = coordinates
+            bed.temp_bed_coordinates = _(
+                '${a}, Block ${b}, Room ${c}, Bed ${d}', mapping = {
+                'a':hall_title, 'b':block,
+                'c':room_nr, 'd':bed_nr})
+        return available_beds
+
+    def update(self, SUBMIT=None):
+        student = self.context.student
+        students_utils = getUtility(IStudentsUtils)
+        self.acc_details  = students_utils.getAccommodationDetails(student)
+        error_message = students_utils.checkAccommodationRequirements(
+            student, self.acc_details)
+        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
+        entries = cat.searchResults(
+            owner=(student.student_id,student.student_id))
+        self.show_available_beds = False
+        if error_message:
+            self.flash(error_message, type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if self.with_ac:
+            self.ac_series = self.request.form.get('ac_series', None)
+            self.ac_number = self.request.form.get('ac_number', None)
+        available_beds = self.getAvailableBeds
+        if SUBMIT is None:
+            if self.with_bedselection and available_beds and not len(entries):
+                self.show_available_beds = True
+            return
+        if self.with_ac:
+            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+            code = get_access_code(pin)
+            if not code:
+                self.flash(_('Activation code is invalid.'), type="warning")
+                return
+        # Search and book bed
+        if len(entries):
+            # If bed space has been manually allocated use this bed ...
+            manual = True
+            bed = list(entries)[0]
+        else:
+            # ... else search for available beds
+            manual = False
+            selected_bed = self.request.form.get('bed', None)
+            if selected_bed:
+                # Use selected bed
+                beds = cat.searchResults(
+                    bed_id=(selected_bed,selected_bed))
+                bed = list(beds)[0]
+                bed.bookBed(student.student_id)
+            elif available_beds:
+                # Select bed according to selectBed method
+                students_utils = getUtility(IStudentsUtils)
+                bed = students_utils.selectBed(available_beds)
+                bed.bookBed(student.student_id)
+            else:
+                self.flash(_('There is no free bed in your category ${a}.',
+                    mapping = {'a':self.acc_details['bt']}), type="warning")
+                self.redirect(self.url(self.context))
+                return
+        if self.with_ac:
+            # Mark pin as used (this also fires a pin related transition)
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'),
+                           type="warning")
+                if not manual:
+                    # Release the previously booked bed
+                    bed.owner = NOT_OCCUPIED
+                    # Catalog must be informed
+                    notify(grok.ObjectModifiedEvent(bed))
+                return
+            else:
+                comment = _(u'invalidated')
+                # Here we know that the ac is in state initialized so we do not
+                # expect an exception, but the owner might be different
+                success = invalidate_accesscode(
+                    pin, comment, self.context.student.student_id)
+                if not success:
+                    self.flash(_('You are not the owner of this access code.'),
+                               type="warning")
+                    if not manual:
+                        # Release the previously booked bed
+                        bed.owner = NOT_OCCUPIED
+                        # Catalog must be informed
+                        notify(grok.ObjectModifiedEvent(bed))
+                    return
+        # Create bed ticket
+        bedticket = createObject(u'waeup.BedTicket')
+        if self.with_ac:
+            bedticket.booking_code = pin
+        bedticket.booking_session = self.acc_details['booking_session']
+        bedticket.bed_type = self.acc_details['bt']
+        bedticket.bed = bed
+        hall_title = bed.__parent__.hostel_name
+        coordinates = bed.coordinates[1:]
+        block, room_nr, bed_nr = coordinates
+        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
+            'a':hall_title, 'b':block,
+            'c':room_nr, 'd':bed_nr,
+            'e':bed.bed_type})
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        bedticket.bed_coordinates = translate(
+            bc, 'waeup.kofa',target_language=portal_language)
+        self.context.addBedTicket(bedticket)
+        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
+        self.flash(_('Bed ticket created and bed booked: ${a}',
+            mapping = {'a':bedticket.display_coordinates}))
+        self.redirect(self.url(self.context))
+        return
+
+class BedTicketDisplayFormPage(KofaDisplayFormPage):
+    """ Page to display bed tickets
+    """
+    grok.context(IBedTicket)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
+    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('Bed Ticket for Session ${a}',
+            mapping = {'a':self.context.getSessionString()})
+
+class ExportPDFBedTicketSlip(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IBedTicket)
+    grok.name('bed_allocation_slip.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
+    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    prefix = 'form'
+    omit_fields = (
+        'password', 'suspended', 'phone', 'adm_code',
+        'suspended_comment', 'date_of_birth', 'current_level',
+        'flash_notice')
+
+    @property
+    def title(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        return translate(_('Bed Allocation Data'), 'waeup.kofa',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        #return translate(_('Bed Allocation: '),
+        #    'waeup.kofa', target_language=portal_language) \
+        #    + ' %s' % self.context.bed_coordinates
+        return translate(_('Bed Allocation Slip'),
+            'waeup.kofa', target_language=portal_language) \
+            + ' %s' % self.context.getSessionString()
+
+    def render(self):
+        studentview = StudentBasePDFFormPage(self.context.student,
+            self.request, self.omit_fields)
+        students_utils = getUtility(IStudentsUtils)
+        note = None
+        n = grok.getSite()['hostels'].allocation_expiration
+        if n:
+            note = _("""
+<br /><br /><br /><br /><br /><font size="12">
+Please endeavour to pay your hostel maintenance charge within ${a} days
+ of being allocated a space or else you are deemed to have
+ voluntarily forfeited it and it goes back into circulation to be
+ available for booking afresh!</font>)
+""")
+            note = _(note, mapping={'a': n})
+            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+            note = translate(
+                note, 'waeup.kofa', target_language=portal_language)
+        return students_utils.renderPDF(
+            self, 'bed_allocation_slip.pdf',
+            self.context.student, studentview,
+            omit_fields=self.omit_fields,
+            note=note)
+
+class BedTicketRelocationView(UtilityView, grok.View):
+    """ Callback view
+    """
+    grok.context(IBedTicket)
+    grok.name('relocate')
+    grok.require('waeup.manageHostels')
+
+    # Relocate student if student parameters have changed or the bed_type
+    # of the bed has changed
+    def update(self):
+        success, msg = self.context.relocateStudent()
+        if not success:
+            self.flash(msg, type="warning")
+        else:
+            self.flash(msg)
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentHistoryPage(KofaPage):
+    """ Page to display student history
+    """
+    grok.context(IStudent)
+    grok.name('history')
+    grok.require('waeup.viewStudent')
+    grok.template('studenthistory')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: History', mapping = {'a':self.context.display_fullname})
+
+# Pages for students only
+
+class StudentBaseEditFormPage(KofaEditFormPage):
+    """ View to edit student base data
+    """
+    grok.context(IStudent)
+    grok.name('edit_base')
+    grok.require('waeup.handleStudent')
+    form_fields = grok.AutoFields(IStudentBase).select(
+        'email', 'phone', 'parents_email')
+    label = _('Edit base data')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudentChangePasswordPage(KofaEditFormPage):
+    """ View to edit student passwords
+    """
+    grok.context(IStudent)
+    grok.name('change_password')
+    grok.require('waeup.handleStudent')
+    grok.template('change_password')
+    label = _('Change password')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        form = self.request.form
+        password = form.get('change_password', None)
+        password_ctl = form.get('change_password_repeat', None)
+        if password:
+            validator = getUtility(IPasswordValidator)
+            errors = validator.validate_password(password, password_ctl)
+            if not errors:
+                IUserAccount(self.context).setPassword(password)
+                # Unset temporary password
+                self.context.temp_password = None
+                self.context.writeLogMessage(self, 'saved: password')
+                self.flash(_('Password changed.'))
+            else:
+                self.flash( ' '.join(errors), type="warning")
+        return
+
+class StudentFilesUploadPage(KofaPage):
+    """ View to upload files by student
+    """
+    grok.context(IStudent)
+    grok.name('change_portrait')
+    grok.require('waeup.uploadStudentFile')
+    grok.template('filesuploadpage')
+    label = _('Upload portrait')
+    pnav = 4
+
+    def update(self):
+        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
+        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
+            emit_lock_message(self)
+            return
+        super(StudentFilesUploadPage, self).update()
+        return
+
+class StartClearancePage(KofaPage):
+    grok.context(IStudent)
+    grok.name('start_clearance')
+    grok.require('waeup.handleStudent')
+    grok.template('enterpin')
+    label = _('Start clearance')
+    ac_prefix = 'CLR'
+    notice = ''
+    pnav = 4
+    buttonname = _('Start clearance now')
+    with_ac = True
+
+    @property
+    def all_required_fields_filled(self):
+        if not self.context.email:
+            return _("Email address is missing."), 'edit_base'
+        if not self.context.phone:
+            return _("Phone number is missing."), 'edit_base'
+        return
+
+    @property
+    def portrait_uploaded(self):
+        store = getUtility(IExtFileStore)
+        if store.getFileByContext(self.context, attr=u'passport.jpg'):
+            return True
+        return False
+
+    def update(self, SUBMIT=None):
+        if not self.context.state == ADMITTED:
+            self.flash(_("Wrong state"), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not self.portrait_uploaded:
+            self.flash(_("No portrait uploaded."), type="warning")
+            self.redirect(self.url(self.context, 'change_portrait'))
+            return
+        if self.all_required_fields_filled:
+            arf_warning = self.all_required_fields_filled[0]
+            arf_redirect = self.all_required_fields_filled[1]
+            self.flash(arf_warning, type="warning")
+            self.redirect(self.url(self.context, arf_redirect))
+            return
+        if self.with_ac:
+            self.ac_series = self.request.form.get('ac_series', None)
+            self.ac_number = self.request.form.get('ac_number', None)
+        if SUBMIT is None:
+            return
+        if self.with_ac:
+            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+            code = get_access_code(pin)
+            if not code:
+                self.flash(_('Activation code is invalid.'), type="warning")
+                return
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'),
+                           type="warning")
+                return
+            # Mark pin as used (this also fires a pin related transition)
+            # and fire transition start_clearance
+            comment = _(u"invalidated")
+            # Here we know that the ac is in state initialized so we do not
+            # expect an exception, but the owner might be different
+            if not invalidate_accesscode(pin, comment, self.context.student_id):
+                self.flash(_('You are not the owner of this access code.'),
+                           type="warning")
+                return
+            self.context.clr_code = pin
+        IWorkflowInfo(self.context).fireTransition('start_clearance')
+        self.flash(_('Clearance process has been started.'))
+        self.redirect(self.url(self.context,'cedit'))
+        return
+
+class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
+    """ View to edit student clearance data by student
+    """
+    grok.context(IStudent)
+    grok.name('cedit')
+    grok.require('waeup.handleStudent')
+    label = _('Edit clearance data')
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IPGStudentClearance).omit(
+                'clr_code', 'officer_comment')
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance).omit(
+                'clr_code', 'officer_comment')
+        return form_fields
+
+    def update(self):
+        if self.context.clearance_locked:
+            emit_lock_message(self)
+            return
+        return super(StudentClearanceEditFormPage, self).update()
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash(_('Clearance form has been saved.'))
+        return
+
+    def dataNotComplete(self):
+        """To be implemented in the customization package.
+        """
+        return False
+
+    @action(_('Save and request clearance'), style='primary',
+            warning=_('You can not edit your data after '
+            'requesting clearance. You really want to request clearance now?'))
+    def requestClearance(self, **data):
+        self.applyData(self.context, **data)
+        if self.dataNotComplete():
+            self.flash(self.dataNotComplete(), type="warning")
+            return
+        self.flash(_('Clearance form has been saved.'))
+        if self.context.clr_code:
+            self.redirect(self.url(self.context, 'request_clearance'))
+        else:
+            # We bypass the request_clearance page if student
+            # has been imported in state 'clearance started' and
+            # no clr_code was entered before.
+            state = IWorkflowState(self.context).getState()
+            if state != CLEARANCE:
+                # This shouldn't happen, but the application officer
+                # might have forgotten to lock the form after changing the state
+                self.flash(_('This form cannot be submitted. Wrong state!'),
+                           type="danger")
+                return
+            IWorkflowInfo(self.context).fireTransition('request_clearance')
+            self.flash(_('Clearance has been requested.'))
+            self.redirect(self.url(self.context))
+        return
+
+class RequestClearancePage(KofaPage):
+    grok.context(IStudent)
+    grok.name('request_clearance')
+    grok.require('waeup.handleStudent')
+    grok.template('enterpin')
+    label = _('Request clearance')
+    notice = _('Enter the CLR access code used for starting clearance.')
+    ac_prefix = 'CLR'
+    pnav = 4
+    buttonname = _('Request clearance now')
+    with_ac = True
+
+    def update(self, SUBMIT=None):
+        if self.with_ac:
+            self.ac_series = self.request.form.get('ac_series', None)
+            self.ac_number = self.request.form.get('ac_number', None)
+        if SUBMIT is None:
+            return
+        if self.with_ac:
+            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+            if self.context.clr_code and self.context.clr_code != pin:
+                self.flash(_("This isn't your CLR access code."), type="danger")
+                return
+        state = IWorkflowState(self.context).getState()
+        if state != CLEARANCE:
+            # This shouldn't happen, but the application officer
+            # might have forgotten to lock the form after changing the state
+            self.flash(_('This form cannot be submitted. Wrong state!'),
+                       type="danger")
+            return
+        IWorkflowInfo(self.context).fireTransition('request_clearance')
+        self.flash(_('Clearance has been requested.'))
+        self.redirect(self.url(self.context))
+        return
+
+class StartSessionPage(KofaPage):
+    grok.context(IStudentStudyCourse)
+    grok.name('start_session')
+    grok.require('waeup.handleStudent')
+    grok.template('enterpin')
+    label = _('Start session')
+    ac_prefix = 'SFE'
+    notice = ''
+    pnav = 4
+    buttonname = _('Start now')
+    with_ac = True
+
+    def update(self, SUBMIT=None):
+        if not self.context.is_current:
+            emit_lock_message(self)
+            return
+        super(StartSessionPage, self).update()
+        if not self.context.next_session_allowed:
+            self.flash(_("You are not entitled to start session."),
+                       type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if self.with_ac:
+            self.ac_series = self.request.form.get('ac_series', None)
+            self.ac_number = self.request.form.get('ac_number', None)
+        if SUBMIT is None:
+            return
+        if self.with_ac:
+            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+            code = get_access_code(pin)
+            if not code:
+                self.flash(_('Activation code is invalid.'), type="warning")
+                return
+            # Mark pin as used (this also fires a pin related transition)
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'),
+                           type="warning")
+                return
+            else:
+                comment = _(u"invalidated")
+                # Here we know that the ac is in state initialized so we do not
+                # expect an error, but the owner might be different
+                if not invalidate_accesscode(
+                    pin,comment,self.context.student.student_id):
+                    self.flash(_('You are not the owner of this access code.'),
+                               type="warning")
+                    return
+        try:
+            if self.context.student.state == CLEARED:
+                IWorkflowInfo(self.context.student).fireTransition(
+                    'pay_first_school_fee')
+            elif self.context.student.state == RETURNING:
+                IWorkflowInfo(self.context.student).fireTransition(
+                    'pay_school_fee')
+            elif self.context.student.state == PAID:
+                IWorkflowInfo(self.context.student).fireTransition(
+                    'pay_pg_fee')
+        except ConstraintNotSatisfied:
+            self.flash(_('An error occurred, please contact the system administrator.'),
+                       type="danger")
+            return
+        self.flash(_('Session started.'))
+        self.redirect(self.url(self.context))
+        return
+
+class AddStudyLevelFormPage(KofaEditFormPage):
+    """ Page for students to add current study levels
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('add')
+    grok.require('waeup.handleStudent')
+    grok.template('studyleveladdpage')
+    form_fields = grok.AutoFields(IStudentStudyCourse)
+    pnav = 4
+
+    @property
+    def label(self):
+        studylevelsource = StudyLevelSource().factory
+        code = self.context.current_level
+        title = studylevelsource.getTitle(self.context, code)
+        return _('Add current level ${a}', mapping = {'a':title})
+
+    def update(self):
+        if not self.context.is_current \
+            or self.context.student.studycourse_locked:
+            emit_lock_message(self)
+            return
+        if self.context.student.state != PAID:
+            emit_lock_message(self)
+            return
+        code = self.context.current_level
+        if code is None:
+            self.flash(_('Your data are incomplete'), type="danger")
+            self.redirect(self.url(self.context))
+            return
+        super(AddStudyLevelFormPage, self).update()
+        return
+
+    @action(_('Create course list now'), style='primary')
+    def addStudyLevel(self, **data):
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = self.context.current_level
+        studylevel.level_session = self.context.current_session
+        try:
+            self.context.addStudentStudyLevel(
+                self.context.certificate,studylevel)
+        except KeyError:
+            self.flash(_('This level exists.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        except RequiredMissing:
+            self.flash(_('Your data are incomplete.'), type="danger")
+            self.redirect(self.url(self.context))
+            return
+        self.flash(_('You successfully created a new course list.'))
+        self.redirect(self.url(self.context, str(studylevel.level)))
+        return
+
+class StudyLevelEditFormPage(KofaEditFormPage):
+    """ Page to edit the student study level data by students
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('edit')
+    grok.require('waeup.editStudyLevel')
+    grok.template('studyleveleditpage')
+    pnav = 4
+    placeholder = _('Enter valid course code')
+
+    def update(self, ADD=None, course=None):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        if self.context.student.state != PAID or \
+            not self.context.is_current_level:
+            emit_lock_message(self)
+            return
+        super(StudyLevelEditFormPage, self).update()
+        if ADD is not None:
+            if not course:
+                self.flash(_('No valid course code entered.'), type="warning")
+                return
+            cat = queryUtility(ICatalog, name='courses_catalog')
+            result = cat.searchResults(code=(course, course))
+            if len(result) != 1:
+                self.flash(_('Course not found.'), type="warning")
+                return
+            course = list(result)[0]
+            if course.former_course:
+                self.flash(_('Former courses can\'t be added.'), type="warning")
+                return
+            addCourseTicket(self, course)
+        return
+
+    @property
+    def label(self):
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        level_title = translate(self.context.level_title, 'waeup.kofa',
+            target_language=lang)
+        return _('Edit course list of ${a}',
+            mapping = {'a':level_title})
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
+
+    def _delCourseTicket(self, **data):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No ticket selected.'), type="warning")
+            self.redirect(self.url(self.context, '@@edit'))
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            # Students are not allowed to remove core tickets
+            if id in self.context and \
+                self.context[id].removable_by_student:
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+            self.context.writeLogMessage(
+                self,'removed: %s at %s' %
+                (', '.join(deleted), self.context.level))
+        self.redirect(self.url(self.context, u'@@edit'))
+        return
+
+    @jsaction(_('Remove selected tickets'))
+    def delCourseTicket(self, **data):
+        self._delCourseTicket(**data)
+        return
+
+    def _updateTickets(self, **data):
+        cat = queryUtility(ICatalog, name='courses_catalog')
+        invalidated = list()
+        for value in self.context.values():
+            result = cat.searchResults(code=(value.code, value.code))
+            if len(result) != 1:
+                course = None
+            else:
+                course = list(result)[0]
+            invalid = self.context.updateCourseTicket(value, course)
+            if invalid:
+                invalidated.append(invalid)
+        if invalidated:
+            invalidated_string = ', '.join(invalidated)
+            self.context.writeLogMessage(
+                self, 'course tickets invalidated: %s' % invalidated_string)
+        self.flash(_('All course tickets updated.'))
+        return
+
+    @action(_('Update all tickets'),
+        tooltip=_('Update all course parameters including course titles.'))
+    def updateTickets(self, **data):
+        self._updateTickets(**data)
+        return
+
+    def _registerCourses(self, **data):
+        if self.context.student.is_postgrad and \
+            not self.context.student.is_special_postgrad:
+            self.flash(_(
+                "You are a postgraduate student, "
+                "your course list can't bee registered."), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        students_utils = getUtility(IStudentsUtils)
+        warning = students_utils.warnCreditsOOR(self.context)
+        if warning:
+            self.flash(warning, type="warning")
+            return
+        msg = self.context.course_registration_forbidden
+        if msg:
+            self.flash(msg, type="warning")
+            return
+        IWorkflowInfo(self.context.student).fireTransition(
+            'register_courses')
+        self.flash(_('Course list has been registered.'))
+        self.redirect(self.url(self.context))
+        return
+
+    @action(_('Register course list'), style='primary',
+        warning=_('You can not edit your course list after registration.'
+            ' You really want to register?'))
+    def registerCourses(self, **data):
+        self._registerCourses(**data)
+        return
+
+class CourseTicketAddFormPage2(CourseTicketAddFormPage):
+    """Add a course ticket by student.
+    """
+    grok.name('ctadd')
+    grok.require('waeup.handleStudent')
+    form_fields = grok.AutoFields(ICourseTicketAdd)
+
+    def update(self):
+        if self.context.student.state != PAID or \
+            not self.context.is_current_level:
+            emit_lock_message(self)
+            return
+        super(CourseTicketAddFormPage2, self).update()
+        return
+
+    @action(_('Add course ticket'))
+    def addCourseTicket(self, **data):
+        # Safety belt
+        if self.context.student.state != PAID:
+            return
+        course = data['course']
+        if course.former_course:
+            self.flash(_('Former courses can\'t be added.'), type="warning")
+            return
+        success = addCourseTicket(self, course)
+        if success:
+            self.redirect(self.url(self.context, u'@@edit'))
+        return
+
+class SetPasswordPage(KofaPage):
+    grok.context(IKofaObject)
+    grok.name('setpassword')
+    grok.require('waeup.Anonymous')
+    grok.template('setpassword')
+    label = _('Set password for first-time login')
+    ac_prefix = 'PWD'
+    pnav = 0
+    set_button = _('Set')
+
+    def update(self, SUBMIT=None):
+        self.reg_number = self.request.form.get('reg_number', None)
+        self.ac_series = self.request.form.get('ac_series', None)
+        self.ac_number = self.request.form.get('ac_number', None)
+
+        if SUBMIT is None:
+            return
+        hitlist = search(query=self.reg_number,
+            searchtype='reg_number', view=self)
+        if not hitlist:
+            self.flash(_('No student found.'), type="warning")
+            return
+        if len(hitlist) != 1:   # Cannot happen but anyway
+            self.flash(_('More than one student found.'), type="warning")
+            return
+        student = hitlist[0].context
+        self.student_id = student.student_id
+        student_pw = student.password
+        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+        code = get_access_code(pin)
+        if not code:
+            self.flash(_('Access code is invalid.'), type="warning")
+            return
+        if student_pw and pin == student.adm_code:
+            self.flash(_(
+                'Password has already been set. Your Student Id is ${a}',
+                mapping = {'a':self.student_id}))
+            return
+        elif student_pw:
+            self.flash(
+                _('Password has already been set. You are using the ' +
+                'wrong Access Code.'), type="warning")
+            return
+        # Mark pin as used (this also fires a pin related transition)
+        # and set student password
+        if code.state == USED:
+            self.flash(_('Access code has already been used.'), type="warning")
+            return
+        else:
+            comment = _(u"invalidated")
+            # Here we know that the ac is in state initialized so we do not
+            # expect an exception
+            invalidate_accesscode(pin,comment)
+            IUserAccount(student).setPassword(self.ac_number)
+            student.adm_code = pin
+        self.flash(_('Password has been set. Your Student Id is ${a}',
+            mapping = {'a':self.student_id}))
+        return
+
+class StudentRequestPasswordPage(KofaAddFormPage):
+    """Captcha'd request password page for students.
+    """
+    grok.name('requestpw')
+    grok.require('waeup.Anonymous')
+    grok.template('requestpw')
+    form_fields = grok.AutoFields(IStudentRequestPW).select(
+        'lastname','number','email')
+    label = _('Request password for first-time login')
+
+    def update(self):
+        blocker = grok.getSite()['configuration'].maintmode_enabled_by
+        if blocker:
+            self.flash(_('The portal is in maintenance mode. '
+                        'Password request forms are temporarily disabled.'),
+                       type='warning')
+            self.redirect(self.url(self.context))
+            return
+        # Handle captcha
+        self.captcha = getUtility(ICaptchaManager).getCaptcha()
+        self.captcha_result = self.captcha.verify(self.request)
+        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
+        return
+
+    def _redirect(self, email, password, student_id):
+        # Forward only email to landing page in base package.
+        self.redirect(self.url(self.context, 'requestpw_complete',
+            data = dict(email=email)))
+        return
+
+    def _redirect_no_student(self):
+        # No record found, this is the truth. We do not redirect here.
+        # We are using this method in custom packages
+        # for redirecting alumni to the application section.
+        self.flash(_('No student record found.'), type="warning")
+        return
+
+    def _pw_used(self):
+        # XXX: False if password has not been used. We need an extra
+        #      attribute which remembers if student logged in.
+        return True
+
+    @action(_('Send login credentials to email address'), style='primary')
+    def get_credentials(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        number = data.get('number','')
+        lastname = data.get('lastname','')
+        cat = getUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(reg_number=(number, number)))
+        if not results:
+            results = list(
+                cat.searchResults(matric_number=(number, number)))
+        if results:
+            student = results[0]
+            if getattr(student,'lastname',None) is None:
+                self.flash(_('An error occurred.'), type="danger")
+                return
+            elif student.lastname.lower() != lastname.lower():
+                # Don't tell the truth here. Anonymous must not
+                # know that a record was found and only the lastname
+                # verification failed.
+                self.flash(_('No student record found.'), type="warning")
+                return
+            elif student.password is not None and self._pw_used:
+                self.flash(_('Your password has already been set and used. '
+                             'Please proceed to the login page.'),
+                           type="warning")
+                return
+            # Store email address but nothing else.
+            student.email = data['email']
+            notify(grok.ObjectModifiedEvent(student))
+        else:
+            self._redirect_no_student()
+            return
+
+        kofa_utils = getUtility(IKofaUtils)
+        password = kofa_utils.genPassword()
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = student
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully requested a password for the')
+        if kofa_utils.sendCredentials(IUserAccount(student),
+            password, url_info, msg):
+            email_sent = student.email
+        else:
+            email_sent = None
+        self._redirect(email=email_sent, password=password,
+            student_id=student.student_id)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
+        return
+
+class ParentsUser:
+    pass
+
+class RequestParentsPasswordPage(StudentRequestPasswordPage):
+    """Captcha'd request password page for parents.
+    """
+    grok.name('requestppw')
+    grok.template('requestppw')
+    label = _('Request password for parents access')
+
+    def update(self):
+        super(RequestParentsPasswordPage, self).update()
+        kofa_utils = getUtility(IKofaUtils)
+        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
+        return
+
+    @action(_('Send temporary login credentials to email address'), style='primary')
+    def get_credentials(self, **data):
+        if not self.captcha_result.is_valid:
+            # Captcha will display error messages automatically.
+            # No need to flash something.
+            return
+        number = data.get('number','')
+        lastname = data.get('lastname','')
+        email = data['email']
+        cat = getUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(reg_number=(number, number)))
+        if not results:
+            results = list(
+                cat.searchResults(matric_number=(number, number)))
+        if results:
+            student = results[0]
+            if getattr(student,'lastname',None) is None:
+                self.flash(_('An error occurred.'), type="danger")
+                return
+            elif student.lastname.lower() != lastname.lower():
+                # Don't tell the truth here. Anonymous must not
+                # know that a record was found and only the lastname
+                # verification failed.
+                self.flash(_('No student record found.'), type="warning")
+                return
+            elif email != student.parents_email:
+                self.flash(_('Wrong email address.'), type="warning")
+                return
+        else:
+            self._redirect_no_student()
+            return
+        kofa_utils = getUtility(IKofaUtils)
+        password = kofa_utils.genPassword()
+        mandate = ParentsPasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['student'] = student
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
+        # Send email with credentials
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
+        msg = _('You have successfully requested a parents password for the')
+        # Create a fake user
+        user = ParentsUser()
+        user.name = student.student_id
+        user.title = "Parents of %s" % student.display_fullname
+        user.email = student.parents_email
+        if kofa_utils.sendCredentials(user, password, url_info, msg):
+            email_sent = user.email
+        else:
+            email_sent = None
+        self._redirect(email=email_sent, password=password,
+            student_id=student.student_id)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
+        return
+
+class StudentRequestPasswordEmailSent(KofaPage):
+    """Landing page after successful password request.
+
+    """
+    grok.name('requestpw_complete')
+    grok.require('waeup.Public')
+    grok.template('requestpwmailsent')
+    label = _('Your password request was successful.')
+
+    def update(self, email=None, student_id=None, password=None):
+        self.email = email
+        self.password = password
+        self.student_id = student_id
+        return
+
+class FilterStudentsInDepartmentPage(KofaPage):
+    """Page that filters and lists students.
+    """
+    grok.context(IDepartment)
+    grok.require('waeup.showStudents')
+    grok.name('students')
+    grok.template('filterstudentspage')
+    pnav = 1
+    session_label = _('Current Session')
+    level_label = _('Current Level')
+
+    def label(self):
+        return 'Students in %s' % self.context.longtitle
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = sorted(
+            [(x.title, x.token) for x in vocab_terms], reverse=True)
+        self.sessions += [('All Sessions', 'all')]
+        return
+
+    def _set_level_values(self):
+        vocab_terms = course_levels.by_value.values()
+        self.levels = sorted(
+            [(x.title, x.token) for x in vocab_terms])
+        self.levels += [('All Levels', 'all')]
+        return
+
+    def _searchCatalog(self, session, level):
+        if level not in (10, 999, None):
+            start_level = 100 * (level // 100)
+            end_level = start_level + 90
+        else:
+            start_level = end_level = level
+        cat = queryUtility(ICatalog, name='students_catalog')
+        students = cat.searchResults(
+            current_session=(session, session),
+            current_level=(start_level, end_level),
+            depcode=(self.context.code, self.context.code)
+            )
+        hitlist = []
+        for student in students:
+            hitlist.append(StudentQueryResultItem(student, view=self))
+        return hitlist
+
+    def update(self, SHOW=None, session=None, level=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_level_values()
+        self.hitlist = []
+        self.session_default = session
+        self.level_default = level
+        if SHOW is not None:
+            if session != 'all':
+                self.session = int(session)
+                self.session_string = '%s %s/%s' % (
+                    self.session_label, self.session, self.session+1)
+            else:
+                self.session = None
+                self.session_string = _('in any session')
+            if level != 'all':
+                self.level = int(level)
+                self.level_string = '%s %s' % (self.level_label, self.level)
+            else:
+                self.level = None
+                self.level_string = _('at any level')
+            self.hitlist = self._searchCatalog(self.session, self.level)
+            if not self.hitlist:
+                self.flash(_('No student found.'), type="warning")
+        return
+
+class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
+    """Page that filters and lists students.
+    """
+    grok.context(ICertificate)
+
+    def label(self):
+        return 'Students studying %s' % self.context.longtitle
+
+    def _searchCatalog(self, session, level):
+        if level not in (10, 999, None):
+            start_level = 100 * (level // 100)
+            end_level = start_level + 90
+        else:
+            start_level = end_level = level
+        cat = queryUtility(ICatalog, name='students_catalog')
+        students = cat.searchResults(
+            current_session=(session, session),
+            current_level=(start_level, end_level),
+            certcode=(self.context.code, self.context.code)
+            )
+        hitlist = []
+        for student in students:
+            hitlist.append(StudentQueryResultItem(student, view=self))
+        return hitlist
+
+class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
+    """Page that filters and lists students.
+    """
+    grok.context(ICourse)
+    grok.require('waeup.viewStudent')
+
+    session_label = _('Session')
+    level_label = _('Level')
+
+    def label(self):
+        return 'Students registered for %s' % self.context.longtitle
+
+    def _searchCatalog(self, session, level):
+        if level not in (10, 999, None):
+            start_level = 100 * (level // 100)
+            end_level = start_level + 90
+        else:
+            start_level = end_level = level
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        coursetickets = cat.searchResults(
+            session=(session, session),
+            level=(start_level, end_level),
+            code=(self.context.code, self.context.code)
+            )
+        hitlist = []
+        for ticket in coursetickets:
+            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
+        return list(set(hitlist))
+
+class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
+    """ Clear all students of a department in state 'clearance requested'.
+    """
+    grok.context(IDepartment)
+    grok.name('clearallstudents')
+    grok.require('waeup.clearAllStudents')
+
+    def update(self):
+        cat = queryUtility(ICatalog, name='students_catalog')
+        students = cat.searchResults(
+            depcode=(self.context.code, self.context.code),
+            state=(REQUESTED, REQUESTED)
+            )
+        num = 0
+        for student in students:
+            if getUtility(IStudentsUtils).clearance_disabled_message(student):
+                continue
+            IWorkflowInfo(student).fireTransition('clear')
+            num += 1
+        self.flash(_('%d students have been cleared.' % num))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class EditScoresPage(KofaPage):
+    """Page that allows to edit batches of scores.
+    """
+    grok.context(ICourse)
+    grok.require('waeup.editScores')
+    grok.name('edit_scores')
+    grok.template('editscorespage')
+    pnav = 1
+    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
+
+    def label(self):
+        return '%s tickets in academic session %s' % (
+            self.context.code, self.session_title)
+
+    def _searchCatalog(self, session):
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        # Attention: Also tickets of previous studycourses are found
+        coursetickets = cat.searchResults(
+            session=(session, session),
+            code=(self.context.code, self.context.code)
+            )
+        return list(coursetickets)
+
+    def _extract_uploadfile(self, uploadfile):
+        """Get a mapping of student-ids to scores.
+
+        The mapping is constructed by reading contents from `uploadfile`.
+
+        We expect uploadfile to be a regular CSV file with columns
+        ``student_id`` and ``score`` (other cols are ignored).
+        """
+        result = dict()
+        data = StringIO(uploadfile.read())  # ensure we have something seekable
+        reader = csv.DictReader(data)
+        for row in reader:
+            if not 'student_id' in row or not 'score' in row:
+                continue
+            result[row['student_id']] = row['score']
+        return result
+
+    def _update_scores(self, form):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
+        error = ''
+        if 'UPDATE_FILE' in form:
+            if form['uploadfile']:
+                try:
+                    formvals = self._extract_uploadfile(form['uploadfile'])
+                except:
+                    self.flash(
+                        _('Uploaded file contains illegal data. Ignored'),
+                        type="danger")
+                    return False
+            else:
+                self.flash(
+                    _('No file provided.'), type="danger")
+                return False
+        else:
+            formvals = dict(zip(form['sids'], form['scores']))
+        for ticket in self.editable_tickets:
+            score = ticket.score
+            sid = ticket.student.student_id
+            if sid not in formvals:
+                continue
+            if formvals[sid] == '':
+                score = None
+            else:
+                try:
+                    score = int(formvals[sid])
+                except ValueError:
+                    error += '%s, ' % ticket.student.display_fullname
+            if ticket.score != score:
+                ticket.score = score
+                ticket.student.__parent__.logger.info(
+                    '%s - %s %s/%s score updated (%s)' % (
+                        ob_class, ticket.student.student_id,
+                        ticket.level, ticket.code, score)
+                    )
+        if error:
+            self.flash(
+                _('Error: Score(s) of following students have not been '
+                    'updated (only integers are allowed): %s.' % error.strip(', ')),
+                type="danger")
+        return True
+
+    def _validate_results(self, form):
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
+        user = get_current_principal()
+        if user is None:
+            usertitle = 'system'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+        self.context.results_validated_by = usertitle
+        self.context.results_validation_date = datetime.utcnow()
+        self.context.results_validation_session = self.current_academic_session
+        return
+
+    def _results_editable(self, results_validation_session,
+                         current_academic_session):
+        user = get_current_principal()
+        prm = IPrincipalRoleManager(self.context)
+        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
+        if 'waeup.local.LocalStudentsManager' in roles:
+            return True
+        if results_validation_session \
+            and results_validation_session >= current_academic_session:
+            return False
+        return True
+
+    def update(self,  *args, **kw):
+        form = self.request.form
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if self.context.__parent__.__parent__.score_editing_disabled \
+            or self.context.score_editing_disabled:
+            self.flash(_('Score editing disabled.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        vs = self.context.results_validation_session
+        if not self._results_editable(vs, self.current_academic_session):
+            self.flash(
+                _('Course results have already been '
+                  'validated and can no longer be changed.'),
+                type="danger")
+            self.redirect(self.url(self.context))
+            return
+        self.session_title = academic_sessions_vocab.getTerm(
+            self.current_academic_session).title
+        self.tickets = self._searchCatalog(self.current_academic_session)
+        if not self.tickets:
+            self.flash(_('No student found.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        self.editable_tickets = [
+            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
+        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
+            and not 'VALIDATE_RESULTS' in form:
+            return
+        if 'VALIDATE_RESULTS' in form:
+            if vs and vs >= self.current_academic_session:
+                self.flash(
+                    _('Course results have already been validated.'),
+                    type="danger")
+            for ticket in self.tickets:
+                if ticket.total_score is not None:
+                    break
+                self.flash(
+                    _('No score has been entered.'),
+                    type="danger")
+                return
+            self._validate_results(form)
+            self.flash(_('You successfully validated the course results.'))
+            self.redirect(self.url(self.context))
+            return
+        if not self.editable_tickets:
+            return
+        success = self._update_scores(form)
+        if success:
+            self.flash(_('You successfully updated course results.'))
+        return
+
+class DownloadScoresView(UtilityView, grok.View):
+    """View that exports scores.
+    """
+    grok.context(ICourse)
+    grok.require('waeup.editScores')
+    grok.name('download_scores')
+
+    def _results_editable(self, results_validation_session,
+                         current_academic_session):
+        user = get_current_principal()
+        prm = IPrincipalRoleManager(self.context)
+        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
+        if 'waeup.local.LocalStudentsManager' in roles:
+            return True
+        if results_validation_session \
+            and results_validation_session >= current_academic_session:
+            return False
+        return True
+
+    def update(self):
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if self.context.__parent__.__parent__.score_editing_disabled \
+            or self.context.score_editing_disabled:
+            self.flash(_('Score editing disabled.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'), type="warning")
+            self.redirect(self.url(self.context))
+            return
+        vs = self.context.results_validation_session
+        if not self._results_editable(vs, self.current_academic_session):
+            self.flash(
+                _('Course results have already been '
+                  'validated and can no longer be changed.'),
+                type="danger")
+            self.redirect(self.url(self.context))
+            return
+        site = grok.getSite()
+        exporter = getUtility(ICSVExporter, name='lecturer')
+        self.csv = exporter.export_filtered(site, filepath=None,
+                                 catalog='coursetickets',
+                                 session=self.current_academic_session,
+                                 level=None,
+                                 code=self.context.code)
+        return
+
+    def render(self):
+        filename = 'results_%s_%s.csv' % (
+            self.context.code, self.current_academic_session)
+        self.response.setHeader(
+            'Content-Type', 'text/csv; charset=UTF-8')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        return self.csv
+
+class ExportPDFScoresSlip(UtilityView, grok.View,
+    LocalRoleAssignmentUtilityView):
+    """Deliver a PDF slip of course tickets for a lecturer.
+    """
+    grok.context(ICourse)
+    grok.name('coursetickets.pdf')
+    grok.require('waeup.showStudents')
+
+    def update(self):
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'), type="danger")
+            self.redirect(self.url(self.context))
+            return
+
+    @property
+    def note(self):
+        return
+
+    def data(self, session):
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        # Attention: Also tickets of previous studycourses are found
+        coursetickets = cat.searchResults(
+            session=(session, session),
+            code=(self.context.code, self.context.code)
+            )
+        header = [[_('Matric No.'),
+                   _('Reg. No.'),
+                   _('Fullname'),
+                   _('Status'),
+                   _('Course of Studies'),
+                   _('Level'),
+                   _('Score') ],]
+        tickets = []
+        for ticket in list(coursetickets):
+            row = [ticket.student.matric_number,
+                  ticket.student.reg_number,
+                  ticket.student.display_fullname,
+                  ticket.student.translated_state,
+                  ticket.student.certcode,
+                  ticket.level,
+                  ticket.score]
+            tickets.append(row)
+        return header + sorted(tickets, key=lambda value: value[0]), None
+
+    def render(self):
+        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
+                     if i['local_role'] == 'waeup.local.Lecturer']
+        lecturers = sorted(lecturers)
+        lecturers =  ', '.join(lecturers)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDFCourseticketsOverview(
+            self, 'coursetickets', self.current_academic_session,
+            self.data(self.current_academic_session), lecturers,
+            'landscape', 90, self.note)
+
+class ExportAttendanceSlip(UtilityView, grok.View,
+    LocalRoleAssignmentUtilityView):
+    """Deliver a PDF slip of course tickets in attendance sheet format.
+    """
+    grok.context(ICourse)
+    grok.name('attendance.pdf')
+    grok.require('waeup.showStudents')
+
+    def update(self):
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'), type="danger")
+            self.redirect(self.url(self.context))
+            return
+
+    @property
+    def note(self):
+        return
+
+    def data(self, session):
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        # Attention: Also tickets of previous studycourses are found
+        coursetickets = cat.searchResults(
+            session=(session, session),
+            code=(self.context.code, self.context.code)
+            )
+        header = [[_('S/N'),
+                   _('Matric No.'),
+                   _('Name'),
+                   _('Level'),
+                   _('Course of\nStudies'),
+                   _('Booklet No.'),
+                   _('Signature'),
+                   ],]
+        tickets = []
+        sn = 1
+        ctlist = sorted(list(coursetickets),
+                        key=lambda value: str(value.student.certcode) +
+                                          str(value.student.matric_number))
+        # In AAUE only editable appear on the attendance sheet. Hopefully
+        # this holds for other universities too.
+        editable_tickets = [ticket for ticket in ctlist
+            if ticket.editable_by_lecturer]
+        for ticket in editable_tickets:
+            name = textwrap.fill(ticket.student.display_fullname, 20)
+            row = [sn,
+                  ticket.student.matric_number,
+                  name,
+                  ticket.level,
+                  ticket.student.certcode,
+                  20 * ' ',
+                  27 * ' ',
+                  ]
+            tickets.append(row)
+            sn += 1
+        return header + tickets, None
+
+    def render(self):
+        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
+                     if i['local_role'] == 'waeup.local.Lecturer']
+        lecturers =  ', '.join(lecturers)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDFCourseticketsOverview(
+            self, 'attendance', self.current_academic_session,
+            self.data(self.current_academic_session),
+            lecturers, '', 65, self.note)
+
+class ExportJobContainerOverview(KofaPage):
+    """Page that lists active student data export jobs and provides links
+    to discard or download CSV files.
+
+    """
+    grok.context(VirtualExportJobContainer)
+    grok.require('waeup.showStudents')
+    grok.name('index.html')
+    grok.template('exportjobsindex')
+    label = _('Student Data Exports')
+    pnav = 1
+    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
+
+    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
+        if CREATE1:
+            self.redirect(self.url('@@exportconfig'))
+            return
+        if CREATE2:
+            self.redirect(self.url('@@exportselected'))
+            return
+        if DISCARD and job_id:
+            entry = self.context.entry_from_job_id(job_id)
+            self.context.delete_export_entry(entry)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            self.context.logger.info(
+                '%s - discarded: job_id=%s' % (ob_class, job_id))
+            self.flash(_('Discarded export') + ' %s' % job_id)
+        self.entries = doll_up(self, user=self.request.principal.id)
+        return
+
+class ExportJobContainerJobConfig(KofaPage):
+    """Page that configures a students export job.
+
+    This is a baseclass.
+    """
+    grok.baseclass()
+    grok.require('waeup.showStudents')
+    grok.template('exportconfig')
+    label = _('Configure student data export')
+    pnav = 1
+    redirect_target = ''
+    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(_('All Sessions'), 'all')]
+        self.sessions += sorted(
+            [(x.title, x.token) for x in vocab_terms], reverse=True)
+        return
+
+    def _set_level_values(self):
+        vocab_terms = course_levels.by_value.values()
+        self.levels = [(_('All Levels'), 'all')]
+        self.levels += sorted(
+            [(x.title, x.token) for x in vocab_terms])
+        return
+
+    def _set_semesters_values(self):
+        utils = getUtility(IKofaUtils)
+        self.semesters =[(_('All Semesters'), 'all')]
+        self.semesters += sorted([(value, key) for key, value in
+                      utils.SEMESTER_DICT.items()])
+        return
+
+    def _set_mode_values(self):
+        utils = getUtility(IKofaUtils)
+        self.modes =[(_('All Modes'), 'all')]
+        self.modes += sorted([(value, key) for key, value in
+                      utils.STUDY_MODES_DICT.items()])
+        return
+
+    def _set_paycat_values(self):
+        utils = getUtility(IKofaUtils)
+        self.paycats =[(_('All Payment Categories'), 'all')]
+        self.paycats += sorted([(value, key) for key, value in
+                      utils.PAYMENT_CATEGORIES.items()])
+        return
+
+    def _set_exporter_values(self):
+        # We provide all student exporters, nothing else, yet.
+        # Bursary, Department or Accommodation Officers don't
+        # have the general exportData
+        # permission and are only allowed to export bursary, payments
+        # overview or accommodation data respectively.
+        # This is the only place where waeup.exportAccommodationData,
+        # waeup.exportBursaryData and waeup.exportPaymentsOverview
+        # are used.
+        exporters = []
+        if not checkPermission('waeup.exportData', self.context):
+            if checkPermission('waeup.exportBursaryData', self.context):
+                exporters += [('Bursary Data', 'bursary')]
+            if checkPermission('waeup.exportPaymentsOverview', self.context):
+                exporters += [('School Fee Payments Overview',
+                               'sfpaymentsoverview'),
+                              ('Session Payments Overview',
+                               'sessionpaymentsoverview')]
+            if checkPermission('waeup.exportAccommodationData', self.context):
+                exporters += [('Bed Tickets', 'bedtickets'),
+                              ('Accommodation Payments',
+                               'accommodationpayments')]
+            self.exporters = exporters
+            return
+        STUDENT_EXPORTER_NAMES = getUtility(
+            IStudentsUtils).STUDENT_EXPORTER_NAMES
+        for name in STUDENT_EXPORTER_NAMES:
+            util = getUtility(ICSVExporter, name=name)
+            exporters.append((util.title, name),)
+        self.exporters = exporters
+        return
+
+    @property
+    def faccode(self):
+        return None
+
+    @property
+    def depcode(self):
+        return None
+
+    @property
+    def certcode(self):
+        return None
+
+    def update(self, START=None, session=None, level=None, mode=None,
+               payments_start=None, payments_end=None, ct_level=None, 
+               ct_session=None, ct_semester=None, paycat=None,
+               paysession=None, level_session=None, exporter=None):
+        self._set_session_values()
+        self._set_level_values()
+        self._set_mode_values()
+        self._set_paycat_values()
+        self._set_exporter_values()
+        self._set_semesters_values()
+        if START is None:
+            return
+        ena = exports_not_allowed(self)
+        if ena:
+            self.flash(ena, type='danger')
+            return
+        if payments_start or payments_end:
+            date_format = '%d/%m/%Y'
+            try:
+                datetime.strptime(payments_start, date_format)
+                datetime.strptime(payments_end, date_format)
+            except ValueError:
+                self.flash(_('Payment dates do not match format d/m/Y.'),
+                           type="danger")
+                return
+        if session == 'all':
+            session=None
+        if level == 'all':
+            level = None
+        if mode == 'all':
+            mode = None
+        if (mode,
+            level,
+            session,
+            self.faccode,
+            self.depcode,
+            self.certcode) == (None, None, None, None, None, None):
+            # Export all students including those without certificate
+            job_id = self.context.start_export_job(exporter,
+                                          self.request.principal.id,
+                                          payments_start = payments_start,
+                                          payments_end = payments_end,
+                                          paycat=paycat,
+                                          paysession=paysession,
+                                          ct_level = ct_level,
+                                          ct_session = ct_session,
+                                          ct_semester = ct_semester,
+                                          level_session=level_session,
+                                          )
+        else:
+            job_id = self.context.start_export_job(exporter,
+                                          self.request.principal.id,
+                                          current_session=session,
+                                          current_level=level,
+                                          current_mode=mode,
+                                          faccode=self.faccode,
+                                          depcode=self.depcode,
+                                          certcode=self.certcode,
+                                          payments_start = payments_start,
+                                          payments_end = payments_end,
+                                          paycat=paycat,
+                                          paysession=paysession,
+                                          ct_level = ct_level,
+                                          ct_session = ct_session,
+                                          ct_semester = ct_semester,
+                                          level_session=level_session,)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
+            % (ob_class, exporter, session, level, mode, self.faccode,
+            self.depcode, self.certcode, payments_start, payments_end,
+            ct_level, ct_session, paycat, paysession, level_session, job_id))
+        self.flash(_('Export started for students with') +
+                   ' current_session=%s, current_level=%s, study_mode=%s' % (
+                   session, level, mode))
+        self.redirect(self.url(self.redirect_target))
+        return
+
+class ExportJobContainerDownload(ExportCSVView):
+    """Page that downloads a students export csv file.
+
+    """
+    grok.context(VirtualExportJobContainer)
+    grok.require('waeup.showStudents')
+
+class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
+    """Page that configures a students export job in datacenter.
+
+    """
+    grok.name('exportconfig')
+    grok.context(IDataCenter)
+    redirect_target = '@@export'
+
+class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
+    """Page that configures a students export job in datacenter.
+
+    """
+    grok.name('exportselected')
+    grok.context(IDataCenter)
+    redirect_target = '@@export'
+    grok.template('exportselected')
+
+    def update(self, START=None, students=None, exporter=None):
+        self._set_exporter_values()
+        if START is None:
+            return
+        ena = exports_not_allowed(self)
+        if ena:
+            self.flash(ena, type='danger')
+            return
+        try:
+            ids = students.replace(',', ' ').split()
+        except:
+            self.flash(sys.exc_info()[1])
+            self.redirect(self.url(self.redirect_target))
+            return
+        job_id = self.context.start_export_job(
+            exporter, self.request.principal.id, selected=ids)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - selected students exported: %s, job_id=%s' %
+            (ob_class, exporter, job_id))
+        self.flash(_('Export of selected students started.'))
+        self.redirect(self.url(self.redirect_target))
+        return
+
+class FacultiesExportJobContainerJobConfig(
+    DatacenterExportJobContainerJobConfig):
+    """Page that configures a students export job in facultiescontainer.
+
+    """
+    grok.context(VirtualFacultiesExportJobContainer)
+    redirect_target = ''
+
+class FacultiesExportJobContainerSelectStudents(
+    DatacenterExportJobContainerSelectStudents):
+    """Page that configures a students export job in facultiescontainer.
+
+    """
+    grok.context(VirtualFacultiesExportJobContainer)
+    redirect_target = ''
+
+class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
+    """Page that configures a students export job in faculties.
+
+    """
+    grok.context(VirtualFacultyExportJobContainer)
+    redirect_target = ''
+
+    @property
+    def faccode(self):
+        return self.context.__parent__.code
+
+class DepartmentExportJobContainerJobConfig(
+    DatacenterExportJobContainerJobConfig):
+    """Page that configures a students export job in departments.
+
+    """
+    grok.context(VirtualDepartmentExportJobContainer)
+    redirect_target = ''
+
+    @property
+    def depcode(self):
+        return self.context.__parent__.code
+
+class CertificateExportJobContainerJobConfig(
+    DatacenterExportJobContainerJobConfig):
+    """Page that configures a students export job for certificates.
+
+    """
+    grok.context(VirtualCertificateExportJobContainer)
+    grok.template('exportconfig_certificate')
+    redirect_target = ''
+
+    @property
+    def certcode(self):
+        return self.context.__parent__.code
+
+class CourseExportJobContainerJobConfig(
+    DatacenterExportJobContainerJobConfig):
+    """Page that configures a students export job for courses.
+
+    In contrast to department or certificate student data exports the
+    coursetickets_catalog is searched here. Therefore the update
+    method from the base class is customized.
+    """
+    grok.context(VirtualCourseExportJobContainer)
+    grok.template('exportconfig_course')
+    redirect_target = ''
+
+    def _set_exporter_values(self):
+        # We provide only the 'coursetickets' and 'lecturer' exporter
+        # but can add more.
+        exporters = []
+        for name in ('coursetickets', 'lecturer'):
+            util = getUtility(ICSVExporter, name=name)
+            exporters.append((util.title, name),)
+        self.exporters = exporters
+        return
+
+    def _set_session_values(self):
+        # We allow only current academic session
+        academic_session = grok.getSite()['configuration'].current_academic_session
+        if not academic_session:
+            self.sessions = []
+            return
+        x = academic_sessions_vocab.getTerm(academic_session)
+        self.sessions = [(x.title, x.token)]
+        return
+
+    def update(self, START=None, session=None, level=None, mode=None,
+               exporter=None):
+        if not checkPermission('waeup.exportData', self.context):
+            self.flash(_('Not permitted.'), type='danger')
+            self.redirect(self.url(self.context))
+            return
+        self._set_session_values()
+        self._set_level_values()
+        self._set_mode_values()
+        self._set_exporter_values()
+        if not self.sessions:
+            self.flash(
+                _('Academic session not set. '
+                  'Please contact the administrator.'),
+                type='danger')
+            self.redirect(self.url(self.context))
+            return
+        if START is None:
+            return
+        ena = exports_not_allowed(self)
+        if ena:
+            self.flash(ena, type='danger')
+            return
+        if session == 'all':
+            session = None
+        if level == 'all':
+            level = None
+        job_id = self.context.start_export_job(exporter,
+                                      self.request.principal.id,
+                                      # Use a different catalog and
+                                      # pass different keywords than
+                                      # for the (default) students_catalog
+                                      catalog='coursetickets',
+                                      session=session,
+                                      level=level,
+                                      code=self.context.__parent__.code)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - exported: %s (%s, %s, %s), job_id=%s'
+            % (ob_class, exporter, session, level,
+            self.context.__parent__.code, job_id))
+        self.flash(_('Export started for course tickets with') +
+                   ' level_session=%s, level=%s' % (
+                   session, level))
+        self.redirect(self.url(self.redirect_target))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt	(revision 16045)
@@ -0,0 +1,73 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <tal:desired_hostel condition="view/actionsgroup1">
+    <br />
+    <p i18n:translate="">
+    Select and <strong>save the desired hostel</strong> before creating
+    a bed ticket. Your choice cannot be changed afterwards.
+    </p>
+    <div class="form-inline">
+      <br />
+      <div class="form-group">
+        <select class="form-control" id="hostel" name="hostel">
+          <option tal:repeat="hostel view/getHostels"
+                  tal:attributes="value hostel/name; selected hostel/selected">
+          <span tal:replace="hostel/title">USERTITLE
+          </span>
+          </option>
+        </select>
+      </div>
+      <div class="form-group">
+        <span tal:repeat="action view/actions" tal:omit-tag="">
+          <input tal:condition="python:action.label in view.actionsgroup1"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <br />
+  </tal:desired_hostel>
+
+  <tal:choice condition="python: view.desired_hostel and view.with_hostel_selection">
+  <p>
+    <strong i18n:translate="">Favoured Hostel:</strong>
+    <span tal:replace="view/desired_hostel"></span>
+  </p>
+  </tal:choice>
+
+  <h3 i18n:domain="waeup.kofa" i18n:translate="">Bookings</h3>
+  <br />
+
+  <table class="kofa-data-table dataTableManage">
+    <thead>
+    <tr>
+      <th tal:condition="not: layout/isStudent">&nbsp;</th>
+      <th i18n:translate="">Session</th>
+      <th i18n:translate="">Booking Date</th>
+      <th i18n:translate="">Bed Coordinates</th>
+      <th i18n:translate="">Bed Type</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="cl context/values">
+         <td tal:condition="not: layout/isStudent">
+          <input type="checkbox"
+    		 name="val_id"
+    		 tal:attributes="value cl/__name__" />
+        </td>
+        <td> <a tal:attributes="href python: view.url(cl)">
+        <span tal:content="python: cl.getSessionString()">PID</span></a></td>
+        <td tal:content="python: layout.formatDatetime(cl.booking_date)">DATE</td>
+        <td tal:content="cl/display_coordinates">COORDINATES</td>
+        <td tal:content="cl/bed_type">TYPE</td>
+      </tr>
+    </tbody>
+  </table>
+
+  <span tal:repeat="action view/actions"
+        tal:omit-tag="">
+    <input tal:condition="python:action.label in view.actionsgroup2"
+           tal:replace="structure action/render"/>
+  </span>
+
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/accommodationpage.pt	(revision 16045)
@@ -0,0 +1,29 @@
+<tal:choice condition="python: view.desired_hostel and view.with_hostel_selection">
+<p>
+  <strong i18n:translate="">Favoured Hostel:</strong>
+  <span tal:replace="view/desired_hostel"></span>
+</p>
+</tal:choice>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Bookings</h3>
+<br />
+
+<table class="kofa-data-table dataTable">
+  <thead>
+  <tr>
+    <th i18n:translate="">Session</th>
+    <th i18n:translate="">Booking Date</th>
+    <th i18n:translate="">Bed Coordinates</th>
+    <th i18n:translate="">Bed Type</th>
+  </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="cl context/values">
+      <td> <a tal:attributes="href python: view.url(cl)">
+      <span tal:content="python: cl.getSessionString()">PID</span></a></td>
+      <td tal:content="python: layout.formatDatetime(cl.booking_date)">DATE</td>
+      <td tal:content="cl/display_coordinates">COORDINATES</td>
+      <td tal:content="cl/bed_type">TYPE</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basemanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basemanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basemanagepage.pt	(revision 16045)
@@ -0,0 +1,55 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+      <span i18n:translate="">Base Data</span></a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Passport Picture</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+          <tr>
+            <td class="fieldname" i18n:translate="">Password:</td>
+            <td>
+              <input name="password" class="form-control" type="password"  />
+            </td>
+          </tr>
+          <tr>
+            <td class="fieldname" i18n:translate="">Retype Password:</td>
+            <td>
+              <input name="control_password" class="form-control" type="password" />
+            </td>
+          </tr>
+
+        </tbody>
+      </table>
+
+      <div tal:condition="view/availableActions">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render"
+               />
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/basepage.pt	(revision 16045)
@@ -0,0 +1,31 @@
+<table i18n:domain="waeup.kofa" class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <tal:block content="widget/label"/>:
+        </td>
+        <td>
+          <input tal:replace="structure widget" />
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <td class="fieldname" i18n:translate="">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    </tr>
+    <tal:files content="structure provider:files" />
+  </tbody>
+  <tfoot>
+    <tr>
+      <td>
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render" />
+      </td>
+    </tr>
+  </tfoot>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/bedticketaddpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/bedticketaddpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/bedticketaddpage.pt	(revision 16045)
@@ -0,0 +1,56 @@
+<form method="POST" i18n:domain="waeup.kofa">
+  <table class="form-table">
+    <tbody>
+      <tal:show_available_beds condition="view/show_available_beds">
+        <tr>
+          <td i18n:translate="" colspan="2">
+            Select your bed before booking acommodation.
+            Your choice cannot be changed afterwards.
+          </td>
+        </tr>
+        <tr>
+          <td i18n:translate="">
+            Desired Bed Space:
+          </td>
+          <td>
+          <span class="form-inline">
+            <select class="form-control" id="bed" name="bed">
+              <option tal:repeat="bed view/getAvailableBeds"
+                      tal:attributes="value bed/bed_id">
+              <span tal:replace="bed/temp_bed_coordinates">BED</span>
+              </option>
+            </select>
+          </span>
+          </td>
+        </tr>
+      </tal:show_available_beds>
+      <tal:with_ac condition="view/with_ac">
+        <tr>
+          <td i18n:translate="">
+          Activation Code:
+          </td>
+          <td>
+          <span tal:replace="view/ac_prefix">PIN Prefix</span> -
+          <input name="ac_series" type="text" maxlength="3"
+                 class="ac-input ac-series"
+                 value="" tal:attributes="value view/ac_series"/> -
+          <input name="ac_number" type="text" maxlength="10"
+                 class="ac-input ac-number"
+                 value="" tal:attributes="value view/ac_number" />
+          <font color="red"><span tal:replace="view/notice">Notice</span>
+          </font>
+          </td>
+        </tr>
+        <tr>
+          <td>&nbsp;</td>
+          <td i18n:translate="">
+          The activation code can be found on the payment slip.
+          </td>
+        </tr>
+      </tal:with_ac>
+    </tbody>
+  </table>
+  <br />
+  <input type="submit" name="SUBMIT" class="btn btn-primary"
+         tal:attributes="value view/buttonname" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/change_password.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/change_password.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/change_password.pt	(revision 16045)
@@ -0,0 +1,29 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <input type="hidden" name="student_id" value=""
+         tal:attributes="value context/student_id" />
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input name="change_password" class="form-control" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input name="change_password_repeat" class="form-control" type="password" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt	(revision 16045)
@@ -0,0 +1,41 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Clearance Data</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Scans</span>
+      </a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+        </tbody>
+      </table>
+      <br />
+      <div tal:condition="view/availableActions">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render"
+               />
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 16045)
@@ -0,0 +1,80 @@
+<form method="POST" i18n:domain="waeup.kofa" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="student_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="matric_number">with matriculation number</option>
+      <option i18n:translate="" value="current_session">in session</option>
+      <option i18n:translate="" value="depcode">in department</option>
+      <option i18n:translate="" value="current_mode">in study mode</option>
+      <option i18n:translate="" value="suspended">
+        with deactivated account (search term omitted)
+      </option>
+      <option i18n:translate="" value="transcript requested">
+        in state 'transcript requested' (search term omitted)
+      </option>
+      <option i18n:translate="" value="transcript validated">
+        in state 'transcript validated' (search term omitted)
+      </option>
+      <option i18n:translate="" value="graduated">
+        in state 'graduated' (search term omitted)
+      </option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+
+  <p>&nbsp;</p>
+  <div tal:condition="view/hitlist">
+    <h3>Search Results</h3>
+    <input type="hidden" name="old_searchterm"
+         tal:attributes="value view/searchterm" />
+    <input type="hidden" name="old_searchtype"
+         tal:attributes="value view/searchtype" />
+
+    <br /><br />
+    <table class="kofa-data-table dataTableManage">
+      <thead>
+    	<tr>
+    	  <th>&nbsp;</th>
+        <th i18n:translate="">Student Id</th>
+        <th i18n:translate="">Reg. Number</th>
+        <th i18n:translate="">Matric. Number</th>
+        <th i18n:translate="">State</th>
+        <th i18n:translate="">Full Name</th>
+    	</tr>
+      </thead>
+      <tbody>
+    	<tr tal:repeat="item view/hitlist">
+    	  <td><input type="checkbox" name="entries"
+    	             tal:attributes="value item/context/__name__" /></td>
+    	  <td> <a tal:attributes="href item/url">
+          <span tal:content="item/student_id">A123456</span></a>
+        </td>
+        <td class="center"> <a tal:attributes="href item/url">
+          <span tal:content="item/reg_number">9999999</span></a>
+        </td>
+        <td class="center"> <a tal:attributes="href item/url">
+          <span tal:content="item/matric_number">9999999</span></a>
+        </td>
+        <td tal:content="item/state">admitted</td>
+    	  <td tal:content="item/display_fullname">Bob</td>
+    	</tr>
+      </tbody>
+    </table>
+    <input type="checkbox" onClick="toggle(this, 'entries')" />
+    &nbsp;<span  i18n:translate="">Select all</span>
+    <br /><br />
+    <input type="submit" name="remove"
+           tal:attributes="value view/remove_button" class="btn btn-default"
+           onclick="return confirmPost('Are you sure?')"/>
+
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containerpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/containerpage.pt	(revision 16045)
@@ -0,0 +1,68 @@
+<form method="POST" i18n:domain="waeup.kofa" class="form-inline">
+<br />
+  <div class="form-group">
+    <input class="btn btn-primary" type="submit" name="search"
+      tal:attributes="value view/search_button" />
+  </div>
+  <div class="form-group">
+    <select name="searchtype" class="form-control">
+      <option i18n:translate="" value="student_id">with id</option>
+      <option i18n:translate="" value="fullname">with name</option>
+      <option i18n:translate="" value="reg_number">with registration number</option>
+      <option i18n:translate="" value="matric_number">with matriculation number</option>
+      <option i18n:translate="" value="current_session">in session</option>
+      <option i18n:translate="" value="depcode">in department</option>
+      <option i18n:translate="" value="current_mode">in study mode</option>
+      <option i18n:translate="" value="suspended">
+        with deactivated account (search term omitted)
+      </option>
+      <option i18n:translate="" value="transcript requested">
+        in state 'transcript requested' (search term omitted)
+      </option>
+      <option i18n:translate="" value="transcript validated">
+        in state 'transcript validated' (search term omitted)
+      </option>
+      <option i18n:translate="" value="graduated">
+        in state 'graduated' (search term omitted)
+      </option>
+    </select>
+  </div>
+  <div class="form-group">
+    <input type="text" class="form-control" name="searchterm" />
+  </div>
+</form>
+<br />
+<div tal:condition="view/hitlist">
+  <h3 i18n:translate="">Search Results</h3>
+  <input type="hidden" name="old_searchterm"
+       tal:attributes="value view/searchterm" />
+  <input type="hidden" name="old_searchtype"
+       tal:attributes="value view/searchtype" />
+  <table class="kofa-data-table dataTable">
+    <thead>
+    <tr>
+      <th i18n:translate="">Student Id</th>
+      <th i18n:translate="">Reg. Number</th>
+      <th i18n:translate="">Matric. Number</th>
+      <th i18n:translate="">State</th>
+      <th i18n:translate="">Full Name</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr tal:repeat="item view/hitlist">
+      <td> <a tal:attributes="href item/url">
+        <span tal:content="item/student_id">A123456</span></a>
+      </td>
+      <td class="center"> <a tal:attributes="href item/url">
+        <span tal:content="item/reg_number">9999999</span></a>
+      </td>
+      <td class="center"> <a tal:attributes="href item/url">
+        <span tal:content="item/matric_number">9999999</span></a>
+      </td>
+      <td tal:content="item/state">admitted</td>
+      <td tal:content="item/display_fullname">Bob</td>
+    </tr>
+    </tbody>
+  </table>
+</div>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt	(revision 16045)
@@ -0,0 +1,32 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+
+      <tr tal:condition="context/grade">
+        <td class="fieldname">
+          <span i18n:translate="">Grade</span>:
+        </td>
+        <td>
+          <span tal:replace="context/grade">C</span>
+        </td>
+      </tr>
+      <tr tal:condition="python: context.weight is not None">
+        <td class="fieldname">
+          <span i18n:translate="">Weight</span>:
+        </td>
+        <td>
+          <span tal:replace="context/weight">1</span>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/courseticketpage.pt	(revision 16045)
@@ -0,0 +1,21 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+    <tr tal:condition="context/grade">
+      <td class="fieldname">
+        <span i18n:translate="">Grade</span>:
+      </td>
+      <td>
+        <span tal:replace="context/grade">C</span>
+      </td>
+    </tr>
+    <tr tal:condition="python: context.weight is not None">
+      <td class="fieldname">
+        <span i18n:translate="">Weight</span>:
+      </td>
+      <td>
+        <span tal:replace="context/weight">1</span>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/editscorespage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 16045)
@@ -0,0 +1,114 @@
+<p  i18n:domain="waeup.kofa" i18n:translate="edit_scores_info">
+  This page helps you to update your students' course results.
+  You can either update scores by importing a csv file
+  (press 'Help' for further information) or by
+  changing score values in the table below. Only scores
+  of students in state 'courses validated' and academic session
+  <span  i18n:name="current_academic_session"
+         tal:replace="view/session_title"></span>
+  can be modified.
+</p>
+
+<form  i18n:domain="waeup.kofa"  method="POST" enctype="multipart/form-data">
+  <br />
+  <div class="input-group">
+    <div class="input-group-btn">
+      <div class="btn btn-default btn-file" i18n:translate="">
+        Select csv file&hellip;
+        <input type="file" name="uploadfile:file" i18n:name="input_file"/>
+      </div>
+    </div>
+    <input type="text" class="form-control" readonly>
+    <div class="input-group-btn">
+      <input type="submit" name="UPDATE_FILE" i18n:translate=""
+             value="Update editable scores from csv file"
+             class="btn btn-primary" />
+      <button class="btn btn-warning" data-toggle="modal"
+              data-target="#myModal" i18n:translate="">
+        Help
+      </button>
+    </div>
+  </div>
+
+  <!-- Modal -->
+  <div class="modal fade" id="myModal" tabindex="-1" role="dialog"
+       aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal-dialog">
+      <div class="modal-content">
+        <div class="modal-header">
+          <button type="button" class="close" data-dismiss="modal"
+                  aria-hidden="true">&times;</button>
+          <h4 class="modal-title" id="myModalLabel" i18n:translate="">
+            Step-by-step instructions
+          </h4>
+        </div>
+        <div class="modal-body" i18n:translate="lecturer_help">
+          <ol>
+            <li>Download csv file.</li>
+            <li>Open csv file in a text editor or in a spreadsheet
+                programme (Excel or Calc).</li>
+            <li>Edit course results only. Do not modify other entries.
+                Do not remove or add columns. Do not add rows.</li>
+            <li>Save file in same format (csv).
+                Do not switch to any other format (xls, xlsx or ods).</li>
+            <li>Select same file for upload and press the blue 'Update ...'
+                button.</li>
+            <li>The values in the table will be updated. Spot-check if
+                the values in the table correspond with the values in
+                your file.</li>
+            <li>Ready</li>
+          </ol>
+          <strong>Note:</strong>
+          Only course results of students which are in
+          state 'courses validated' and in academic session
+          <span  i18n:name="current_academic_session"
+                 tal:replace="view/session_title"></span>
+          can be modified. Additional data will just be ignored.
+        </div>
+      </div><!-- /.modal-content -->
+    </div><!-- /.modal-dialog -->
+  </div><!-- /.modal -->
+
+  <br /><br />
+
+  <table class="kofa-data-table dataTable">
+    <thead>
+    <tr>
+      <th i18n:translate="">Matric No.</th>
+      <th i18n:translate="">Student Id</th>
+      <th i18n:translate="">Fullname</th>
+      <th i18n:translate="">Status</th>
+      <th i18n:translate="">Course of Studies</th>
+      <th i18n:translate="">Level</th>
+      <th i18n:translate="">Score</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="ticket view/tickets">
+      <td tal:content="ticket/student/matric_number">MATRIC_NUMBER</td>
+      <td tal:content="ticket/student/student_id">STUDENT ID</td>
+      <td tal:content="ticket/student/display_fullname">FULLNAME</td>
+      <td tal:content="ticket/student/translated_state">STATE</td>
+      <td tal:content="ticket/student/certcode">CERTCODE</td>
+      <td tal:content="ticket/level">LEVEL</td>
+
+      <td tal:condition="ticket/editable_by_lecturer" style="width: 65px;">
+          <input type="text" name="scores:list" class="form-control"
+                 tal:attributes="value ticket/score" />
+          <input type="hidden" name="sids:list"
+                 tal:attributes="value ticket/student/student_id" />
+      </td>
+      <td tal:condition="not: ticket/editable_by_lecturer"
+          tal:content="ticket/score">SCORE</td>
+     </tr>
+    </tbody>
+  </table>
+
+  <input type="submit" name="UPDATE_TABLE" i18n:translate=""
+         value="Update scores from table" class="btn btn-primary"
+         />
+
+  <input type="submit" name="VALIDATE_RESULTS" i18n:translate=""
+         value="Validate course results" class="btn btn-primary"
+         />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/enterpin.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/enterpin.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/enterpin.pt	(revision 16045)
@@ -0,0 +1,43 @@
+<form method="POST" i18n:domain="waeup.kofa">
+  <table class="form-table" tal:condition="view/with_ac">
+    <tbody>
+      <tr>
+        <td i18n:translate="">
+        Activation Code:
+        </td>
+        <td>
+        <span tal:replace="view/ac_prefix">PIN Prefix</span> -
+        <input name="ac_series" type="text" maxlength="3"
+               class="ac-input ac-series"
+               value="" tal:attributes="value view/ac_series"/> -
+        <input name="ac_number" type="text" maxlength="10"
+               class="ac-input ac-number"
+               value="" tal:attributes="value view/ac_number" />
+        <font color="red"><span tal:replace="view/notice">Notice</span>
+        </font>
+        </td>
+      </tr>
+      <tr>
+        <td>&nbsp;</td>
+        <td i18n:translate="">
+        The activation code can be found on the payment slip.
+        </td>
+      </tr>
+      <tr>
+        <td colspan="2">
+          <input type="submit" name="SUBMIT" class="btn btn-primary"
+                 tal:attributes="value view/buttonname" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <span tal:omit-tag="" tal:condition="not: view/with_ac">
+    <p i18n:translate="">
+      Your data have been approved. You can continue by
+      clicking the button below.
+    </p>
+    <input type="submit" name="SUBMIT" class="btn btn-primary"
+           tal:attributes="value view/buttonname" />
+  </span>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 16045)
@@ -0,0 +1,189 @@
+<p class="alert alert-warning"  i18n:domain="waeup.kofa"
+   i18n:translate="student_export_warning">
+  <strong>NOTICE:</strong>
+  Student data exports are very computation intensive.
+  Depending on the filter selected and the portal traffic load, exports may take
+  longer than 15 minutes. You are advised to export data outside high-traffic
+  time periods.
+</p>
+
+<br />
+
+<form i18n:domain="waeup.kofa" method="POST">
+  <label for="mode" i18n:translate="">
+    Exporter (Data Type)
+  </label>
+  <select id="exporter" name="exporter" class="form-control half" onclick="test()">
+    <option tal:repeat="item view/exporters"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">Students</span>
+    </option>
+  </select>
+  <br />
+  <label for="session" i18n:translate="">
+    Current Session
+  </label>
+  <select name="session" class="form-control half">
+    <option tal:repeat="item view/sessions"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">2012/2013</span>
+    </option>
+  </select>
+  <br />
+  <label for="level" i18n:translate="">
+    Current Level
+  </label>
+  <select name="level" class="form-control half">
+    <option tal:repeat="item view/levels"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">200</span>
+    </option>
+  </select>
+  <br />
+  <label for="mode" i18n:translate="">
+    Current Study Mode
+  </label>
+  <select name="mode" class="form-control half">
+    <option tal:repeat="item view/modes"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">ug_ft</span>
+    </option>
+  </select>
+
+  <span id="payment_dates" style="display: none;">
+    <br />
+    <label for="mode" i18n:translate="">
+      Payment Date
+    </label>
+    <br />
+    from
+    <input class="datepicker-le-year datepicker-short textType"
+           id="payments_start"
+           name="payments_start" type="text" value="" />
+    to <input class="datepicker-le-year datepicker-short textType"
+           id="payments_end"
+           name="payments_end" type="text" value="" />
+    <br />
+  </span>
+
+  <span id="paycat" style="display: none;">
+    <br />
+    <label for="paycat" i18n:translate="">
+      Payment Categories
+    </label>
+    <select name="paycat" class="form-control half">
+      <option tal:repeat="item view/paycats"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">schoolfee</span>
+      </option>
+    </select>
+    <br />
+  </span>
+
+  <span id="paysession" style="display: none;">
+    <br />
+    <label for="paysession" i18n:translate="">
+      Payment Session
+    </label>
+    <select name="paysession" class="form-control half">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2018/2019</span>
+      </option>
+    </select>
+    <br />
+  </span>
+
+  <span id="level_session" style="display: none;">
+    <br />
+    <label for="level_session" i18n:translate="">
+      Level Session
+    </label>
+    <select name="level_session" class="form-control half">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2018/2019</span>
+      </option>
+    </select>
+    <br />
+  </span>
+
+  <span id="ct_data" style="display: none;">
+    <br />
+    <label for="mode" i18n:translate="">
+      Course Ticket Level
+    </label>
+    <select name="ct_level" class="form-control half">
+      <option tal:repeat="item view/levels"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">200</span>
+      </option>
+    </select>
+    <br />
+    <label for="mode" i18n:translate="">
+      Course Ticket Session
+    </label>
+    <select name="ct_session" class="form-control half">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2012/2013</span>
+      </option>
+    </select>
+    <br />
+    <label for="semester" i18n:translate="">
+      Course Ticket Semester
+    </label>
+    <select name="ct_semester" class="form-control half">
+      <option tal:repeat="item view/semesters"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">1st Semester</span>
+      </option>
+    </select>
+    <br />
+  </span>
+
+  <br />
+
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn btn-primary" />
+</form>
+
+<script type="text/javascript">
+  function test() {
+      if (document.getElementById('exporter').value == 'bursary' ||
+          document.getElementById('exporter').value == 'studentpayments' ||
+          document.getElementById('exporter').value == 'accommodationpayments' ||
+          document.getElementById('exporter').value == 'trimmedpayments' ||
+          document.getElementById('exporter').value == 'unpaidpayments') {
+          document.getElementById('payment_dates').style.display = 'block';
+          document.getElementById('paycat').style.display = 'block';
+          document.getElementById('paysession').style.display = 'block';
+          document.getElementById('ct_data').style.display = 'none';
+          document.getElementById('level_session').style.display = 'none';
+
+      } else if (document.getElementById('exporter').value == 'coursetickets')  {
+          document.getElementById('ct_data').style.display = 'block';
+          document.getElementById('payment_dates').style.display = 'none';
+          document.getElementById('paycat').style.display = 'none';
+          document.getElementById('paysession').style.display = 'none';
+          document.getElementById('level_session').style.display = 'none';
+
+      } else if (document.getElementById('exporter').value == 'studentstudylevels' ||
+          document.getElementById('exporter').value == 'levelreportdata') {
+          document.getElementById('ct_data').style.display = 'none';
+          document.getElementById('payment_dates').style.display = 'none';
+          document.getElementById('paycat').style.display = 'none';
+          document.getElementById('paysession').style.display = 'none';
+          document.getElementById('level_session').style.display = 'block';
+
+      } else {
+          document.getElementById('ct_data').style.display = 'none';
+          document.getElementById('payment_dates').style.display = 'none';
+          document.getElementById('paycat').style.display = 'none';
+          document.getElementById('paysession').style.display = 'none';
+          document.getElementById('level_session').style.display = 'none';
+      }
+  }
+</script>
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 16045)
@@ -0,0 +1,73 @@
+<p class="alert alert-warning"  i18n:domain="waeup.kofa"
+   i18n:translate="student_export_warning">
+  <strong>NOTICE:</strong>
+  Student data exports are very computation intensive.
+  Depending on the filter selected and the portal traffic load, exports may take
+  longer than 15 minutes. You are advised to export data outside high-traffic
+  time periods.
+</p>
+
+<br />
+
+<form i18n:domain="waeup.kofa" method="POST">
+  <label for="mode" i18n:translate="">
+    Exporter (Data Type)
+  </label>
+  <select id="exporter" name="exporter" class="form-control half" onclick="test()">
+    <option tal:repeat="item view/exporters"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">Students</span>
+    </option>
+  </select>
+  <br />
+  <label for="session" i18n:translate="">
+    Current Session
+  </label>
+  <select name="session" class="form-control half">
+    <option tal:repeat="item view/sessions"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">2012/2013</span>
+    </option>
+  </select>
+  <br />
+  <label for="level" i18n:translate="">
+    Current Level
+  </label>
+  <select name="level" class="form-control half">
+    <option tal:repeat="item view/levels"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">200</span>
+    </option>
+  </select>
+  <br />
+  <span id="payment_dates" style="display: none;">
+    <label for="mode" i18n:translate="">
+      Payment Date
+    </label>
+    <br />
+    from
+    <input class="datepicker-le-year datepicker-short textType"
+           id="payments_start"
+           name="payments_start" type="text" value="" />
+    to <input class="datepicker-le-year datepicker-short textType"
+           id="payments_end"
+           name="payments_end" type="text" value="" />
+    <br />
+  </span>
+  <br />
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn btn-primary" />
+</form>
+
+<script type="text/javascript">
+  function test() {
+      if (document.getElementById('exporter').value == 'bursary' ||
+          document.getElementById('exporter').value == 'studentpayments' ||
+          document.getElementById('exporter').value == 'studentunpaidpayments') {
+          document.getElementById('payment_dates').style.display = 'block';
+      } else {
+          document.getElementById('payment_dates').style.display = 'none';
+      }
+  }
+</script>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_course.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_course.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportconfig_course.pt	(revision 16045)
@@ -0,0 +1,46 @@
+<p class="alert alert-warning"  i18n:domain="waeup.kofa"
+   i18n:translate="student_export_warning">
+  <strong>NOTICE:</strong>
+  Student data exports are very computation intensive.
+  Depending on the filter selected and the portal traffic load, exports may take
+  longer than 15 minutes. You are advised to export data outside high-traffic
+  time periods.
+</p>
+
+<br />
+
+<form i18n:domain="waeup.kofa" method="POST">
+  <label for="mode" i18n:translate="">
+    Exporter (Data Type)
+  </label>
+  <select name="exporter" class="form-control half">
+    <option tal:repeat="item view/exporters"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">Students</span>
+    </option>
+  </select>
+  <br />
+  <label for="session" i18n:translate="">
+    Level Session
+  </label>
+  <select name="session" class="form-control half">
+    <option tal:repeat="item view/sessions"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">2012/2013</span>
+    </option>
+  </select>
+  <br />
+  <label for="level" i18n:translate="">
+    Level
+  </label>
+  <select name="level" class="form-control half">
+    <option tal:repeat="item view/levels"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">200</span>
+    </option>
+  </select>
+  <br /><br />
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn btn-primary" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportjobsindex.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportjobsindex.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportjobsindex.pt	(revision 16045)
@@ -0,0 +1,64 @@
+<table i18n:domain="waeup.kofa" class="table table-condensed">
+  <thead>
+    <tr>
+      <th i18n:translate="">Export Number</th>
+      <th i18n:translate="">Exporter</th>
+      <th i18n:translate="">Filter</th>
+      <th i18n:translate="">Creator</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Status</th>
+      <th>&nbsp;</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="job view/entries">
+      <td>
+        <span tal:replace="job/job">12</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/exporter">EXPORTER</span>
+      </td>
+      <td>
+        <span tal:replace="structure job/args">ARGS</span>
+      </td>
+      <td>
+        <span tal:replace="job/creator">CREATOR</span>
+      </td>
+      <td>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn btn-default" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img src="/static/img/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn btn-primary btn-xs"
+                 tal:attributes="href job/download_url"
+                 tal:condition="job/show_download_button">
+            Download</a>
+
+          <input type="hidden" name="job_id"
+                 tal:attributes="value job/job" />
+          <input type="submit" class="btn btn-default btn-xs"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<form method="POST" i18n:domain="waeup.kofa">
+  <input class="btn btn-primary" type="submit" name="CREATE1"
+         value="Set export parameters"
+          />
+  <input tal:condition="python:context.__parent__.__name__ == 'faculties'"
+         class="btn btn-primary" type="submit" name="CREATE2"
+         value="Enter student ids or matric numbers"
+          />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportselected.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportselected.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/exportselected.pt	(revision 16045)
@@ -0,0 +1,27 @@
+<p i18n:translate="">
+  Here you can export student data for a small subset of students by entering
+  a list of student ids or matric numbers.
+  The values must be separated by whitespaces or commas.
+  You can also copy and paste parts of Excel or Calc columns.
+</p>
+
+<form i18n:domain="waeup.kofa" method="POST">
+  <label for="mode" i18n:translate="">
+    Exporter (Data Type)
+  </label>
+  <select id="exporter" name="exporter" class="form-control half" onclick="test()">
+    <option tal:repeat="item view/exporters"
+            tal:attributes="value python:item[1]">
+      <span tal:replace="python: item[0]">Students</span>
+    </option>
+  </select>
+  <br />
+  <label for="mode" i18n:translate="">
+    Student Ids or Matric Numbers
+  </label>
+  <input name="students" type="text" class="form-control" id="students"
+  <br /><br />
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn btn-primary" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filesuploadpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filesuploadpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filesuploadpage.pt	(revision 16045)
@@ -0,0 +1,8 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <table class="form-table">
+    <tbody>
+      <tal:files content="structure provider:files" />
+    </tbody>
+  </table>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filterstudentspage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filterstudentspage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/filterstudentspage.pt	(revision 16045)
@@ -0,0 +1,82 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <br />
+  <label for="session">
+    <span tal:replace="view/session_label">SESSION</span>
+  </label>
+  <select name="session" class="form-control half">
+    <span tal:repeat="item view/sessions" tal:omit-tag="">
+      <option
+          tal:define="name python: item[1];
+                      title python: item[0];
+                      selected python: view.session_default == name"
+          tal:attributes="value name; selected selected">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="levels">
+    <span tal:replace="view/level_label">LEVEL</span>
+  </label>
+  <select name="level" class="form-control half">
+    <span tal:repeat="item view/levels" tal:omit-tag="">
+      <option
+          tal:define="name python: item[1];
+                      title python: item[0];
+                      selected python: view.level_default == name"
+          tal:attributes="value name; selected selected">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br /><br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="SHOW" value="Show" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+
+  <br /><br />
+
+  <div tal:condition="view/hitlist">
+    <h3>
+      ...
+      <span tal:replace="view/session_string">SESSION</span>
+      <span i18n:translate="">and</span>
+      <span tal:replace="view/level_string">LEVEL</span>
+    </h3>
+    <table class="kofa-data-table dataTable">
+      <thead>
+    	<tr>
+    	  <th i18n:translate="">Student Id</th>
+        <th i18n:translate="">Matric No.</th>
+        <th i18n:translate="">Fullname</th>
+        <th i18n:translate="">Status</th>
+        <th i18n:translate="">Certificate</th>
+        <th i18n:translate="">Current Session</th>
+        <th i18n:translate="">Current Level</th>
+        <th i18n:translate="">Comment</th>
+    	</tr>
+      </thead>
+      <tbody>
+    	<tr tal:repeat="item view/hitlist">
+    	  <td> <a tal:attributes="href item/url"
+                target="_blank">
+          <span tal:content="item/student_id">A123456</span></a>
+        </td>
+        <td tal:content="item/matric_number">MATRIC_NUMBER</td>
+    	  <td tal:content="item/display_fullname">FULLNAME</td>
+        <td tal:content="item/translated_state">STATE</td>
+        <td>
+          <a tal:attributes="href python: view.url(item.certificate)"
+             tal:content="item/certificate/code">
+             CERTIFICATE
+          </a>
+        </td>
+        <td tal:content="item/current_session">SESSION</td>
+        <td tal:content="item/current_level">LEVEL</td>
+        <td><span tal:condition="item/comment">yes</span></td>
+    	</tr>
+      </tbody>
+    </table>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imagedisplay.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imagedisplay.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imagedisplay.pt	(revision 16045)
@@ -0,0 +1,10 @@
+<tr>
+  <td class="fieldname">
+        <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+      <img height="125px"
+         tal:attributes="src python: view.url(context, viewlet.download_name)" />
+      <br />
+  </td>
+</tr>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imageupload.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imageupload.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/imageupload.pt	(revision 16045)
@@ -0,0 +1,33 @@
+<tr i18n:domain="waeup.kofa">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <img align="middle" height="125px"
+         tal:attributes="src python: view.url(context, viewlet.download_name)" />
+    &nbsp;
+    <input type="submit" tal:condition="viewlet/file_exists"
+     tal:attributes="id python:('delete_%s' % viewlet.input_name);
+        name python:('delete_%s' % viewlet.input_name)"
+     value="Delete" class="btn btn-danger" />
+    <br /><br />
+
+		<div class="input-group">
+			<div class="input-group-btn">
+				<div class="btn btn-default btn-file">
+          Select new file&hellip;
+          <input type="file" tal:attributes="name viewlet/input_name" />
+        </div>
+			</div>
+			<input type="text" class="form-control" readonly>
+      <div class="input-group-btn">
+        <input type="submit"
+               tal:attributes="id python:('upload_%s' % viewlet.input_name);
+                name python:('upload_%s' % viewlet.input_name);
+                value viewlet/upload_button"
+               class="btn btn-primary" />
+        (<span tal:replace="viewlet/max_upload_size">10 kB</span> max.)
+      </div>
+    </div>
+  </td>
+</tr>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep1.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep1.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep1.pt	(revision 16045)
@@ -0,0 +1,18 @@
+<form i18n:domain="waeup.kofa" method="post">
+  <p i18n:translate="login_as_intro_1">
+    Kofa is creating a temporary student password which will replace the
+    original password chosen by the student. The password will be valid
+    for
+    <span i18n:name="minutes"
+          tal:replace=view/temp_password_minutes></span>
+    minutes. The student can't login
+    during this time period. After expiration the student can login
+    with the original password again.
+  </p>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep2.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep2.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/loginasstep2.pt	(revision 16045)
@@ -0,0 +1,13 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <p i18n:translate="login_as_intro_2">
+    The random password
+    "<span i18n:name="password"
+          tal:replace=view/password></span>"
+    has been set. You can proceed by clicking the button below.
+  </p>
+  <input type="hidden" name="form.login" tal:attributes="value view/context/student_id" />
+  <input type="hidden" name="form.password"   tal:attributes="value view/password" />
+  <input class="btn btn-primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt	(revision 16045)
@@ -0,0 +1,13 @@
+<li  tal:attributes="class viewlet/active">
+  <a href="#" class="dropdown-toggle"
+     data-toggle="dropdown">
+     <span tal:replace="viewlet/tab_title">My Data</span>
+     <b class="caret"></b>
+  </a>
+  <ul class="dropdown-menu">
+    <li tal:repeat="target viewlet/targets">
+      <a tal:attributes="href target/url"
+         tal:content="target/title">LINKTITLE</a>
+    </li>
+  </ul>
+</li>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt	(revision 16045)
@@ -0,0 +1,67 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          Payment Category:
+        </td>
+        <td>
+          <select id="form.p_category" name="form.p_category" size="1"
+                  class="form-control half" onclick="further()">
+            <option tal:repeat="item view/selectable_categories"
+                    tal:attributes="value python:item[0]">
+              <span tal:replace="python:item[1]">FEE CATEGORY</span>
+            </option>
+          </select>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <table id="widgets" class="form-table" style="display: none;">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+
+  <span id="p_item" style="display: none;">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          Desired Course of Study:
+        </td>
+        <td>
+          <input class="textType" id="new_programme" name="new_programme"
+            type="text">
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  </span>
+
+  <br />
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
+
+<script type="text/javascript">
+  function further() {
+      if (document.getElementById('form.p_category').value == 'transfer') {
+          document.getElementById('p_item').style.display = 'block';
+      } else {
+          document.getElementById('p_item').style.display = 'none';
+      }
+      if (document.getElementById('form.p_category').value == 'combi') {
+          document.getElementById('widgets').style.display = 'block';
+      } else {
+          document.getElementById('widgets').style.display = 'none';
+      }
+  }
+</script>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt	(revision 16045)
@@ -0,0 +1,45 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="kofa-data-table dataTableManage">
+    <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th i18n:translate="">Payment Id</th>
+      <th i18n:translate="">Creation Date</th>
+      <th i18n:translate="">Payment Date</th>
+      <th i18n:translate="">Category</th>
+      <th i18n:translate="">Item</th>
+      <th i18n:translate="">Amount</th>
+      <th i18n:translate="">Session</th>
+      <th i18n:translate="">State</th>
+    </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="cl context/values">
+         <td>
+          <input type="checkbox"
+             name="val_id"
+             tal:attributes="value cl/__name__"
+         tal:condition="python: not view.unremovable(cl)" />
+        </td>
+        <td> <a tal:attributes="href python: view.url(cl)">
+        <span tal:content="cl/p_id">PID</span></a></td>
+        <td tal:content="python: layout.formatDatetime(cl.creation_date)">CREATION DATE</td>
+        <td tal:content="python: layout.formatDatetime(cl.payment_date)">PAYMENT DATE</td>
+      <td tal:content ="cl/category">CATEGORY</td>
+      <td tal:content ="cl/display_item">ITEM</td>
+      <td tal:content ="cl/amount_auth">AMOUNT</td>
+      <td tal:content ="cl/p_session">SESSION</td>
+      <td tal:content ="cl/p_state_title">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="python: view.availableActions and view.manage_payments_allowed">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestppw.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestppw.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestppw.pt	(revision 16045)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <p i18n:translate="">
+    The student record of your child will be looked up and
+    temporary login credentials will be sent to the email address given above.
+    To be able to proceed you must enter the parents email address stored in Kofa.
+    After login, you'll only be able to view certain data. You won't be able
+    to edit data, register courses or make payments. The login credentials will
+    be valid for
+    <span i18n:name="minutes"
+          tal:replace=view/temp_password_minutes></span>
+    minutes. After this time you'll be automatically logged out.
+  </p>
+  <div tal:content="structure view/captcha_code"></div>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpw.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 16045)
@@ -0,0 +1,21 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <p i18n:translate="">
+    Your student record will be looked up and
+    your login credentials will be sent to the email address given above.
+    To be able to proceed you must provide a valid email address!
+  </p>
+  <div tal:content="structure view/captcha_code"></div>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 16045)
@@ -0,0 +1,47 @@
+<tal:only_email condition = "not: view/password" i18n:domain="waeup.kofa">
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p tal:condition = "view/email" i18n:translate="">
+    Please check your email account to proceed.
+  </p>
+  <p tal:condition = "not: view/email" i18n:translate="">
+    Email could not been sent. Please retry later.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:only_email>
+
+<tal:credentials condition = "view/password" i18n:domain="waeup.kofa">
+  <h3 i18n:translate="">Your login credentials are:</h3>
+  <table class="bordered-table span6">
+    <tr>
+      <td i18n:translate="">User Name:</td>
+      <td tal:content="view/applicant_id">ID</td>
+    <tr>
+    <tr>
+      <td i18n:translate="">Password:</td>
+      <td tal:content="view/password">PASSWORD</td>
+    <tr>
+  </table>
+  <p>
+    <span i18n:translate="">Print this page and proceed to the</span>
+    <a tal:attributes="href python: view.url(layout.site, 'login')">login form</a>.
+    <span i18n:translate="">Please note that passwords are case-sensitive,
+    <br />when entering your credentials, and keep your password secret!
+    </span>
+  </p>
+  <p tal:condition = "view/email">
+    <span i18n:translate=""> An email with your user name and password
+    has been sent to
+    </span>
+    <span tal:replace="view/email">user@foo.baz</span>.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using Kofa!
+  </p>
+</tal:credentials>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/reverttransfer.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/reverttransfer.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/reverttransfer.pt	(revision 16045)
@@ -0,0 +1,16 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+    <p i18n:translate="">
+      Student transfers can be reverted one after the other. When reverting
+      a transfer, the active study course will be entirely removed including
+      all containing course lists (study levels)
+      and the previous study course will be reactivated.
+    </p>
+    <br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/setpassword.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/setpassword.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/setpassword.pt	(revision 16045)
@@ -0,0 +1,48 @@
+<form i18n:domain="waeup.kofa" method="POST">
+  <table class="form-table">
+    <tbody>
+      <tr>
+      <td class="fieldname" i18n:translate="">
+          Registration Number:
+      </td>
+      <td>
+        <input name="reg_number" type="text"
+               value="" tal:attributes="value view/reg_number"/>
+      </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+          Access Code:
+        </td>
+        <td>
+          <span tal:replace="view/ac_prefix">
+            PINPREFIX
+          </span> -
+          <input name="ac_series" type="text"
+                 class="ac-input ac-series"
+                 value="" tal:attributes="value view/ac_series"/> -
+          <input name="ac_number" type="text"
+                 class="ac-input ac-number"
+                 value="" tal:attributes="value view/ac_number" /> </td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" name="SUBMIT" class="btn btn-primary"
+         tal:attributes="value view/set_button" />
+</form>
+<div i18n:domain="waeup.kofa" >
+  <h3 i18n:translate="">Instructions:</h3>
+  <ul i18n:translate="">
+    <li> Acquire a password access code (PWD).
+    </li>
+    <li> Enter your (JAMB) registration number and the access code.
+    </li>
+    <li> After pressing the button, your student id will be displayed
+    and your password set. The 10-digit part of the access code just
+    used is your new password.
+    </li>
+    <li> Go to the <strong><a href="login">general login
+    page</a></strong> and enter your new credentials.
+    </li>
+  </ul>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studenthistory.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studenthistory.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studenthistory.pt	(revision 16045)
@@ -0,0 +1,5 @@
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt	(revision 16045)
@@ -0,0 +1,103 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Study Course Data</span>
+      </a>
+    </li>
+    <li t>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Study Levels</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+      <tbody>
+        <tal:widgets content="structure provider:widgets" />
+      </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th>
+            <th i18n:translate="">Level Code</th>
+            <th i18n:translate="">Level Title</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="cl context/values">
+             <td>
+              <input type="checkbox"
+        		 name="val_id"
+        		 tal:attributes="value cl/__name__" />
+            </td>
+            <td> <a tal:attributes="href python:view.url(cl)">
+              <span tal:content="cl/level">CODE</span></a>
+            </td>
+            <td>
+              <span tal:content="cl/level_title">TITLE</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+      <br /><br /><br />
+
+
+      <div class="form-inline">
+        <span tal:condition="view/availableActions">
+          <div tal:repeat="action view/actions"
+              class="form-group">
+              <input tal:condition="python:action.label in view.tabthreeactions"
+                     tal:replace="structure action/render"/>
+          </div>
+        </span>
+        <div class="form-group">
+          <select id="addlevel" name="addlevel" class="form-control">
+            <option tal:attributes="value level/code" tal:repeat="level view/level_dicts">
+              <span tal:replace="level/title">LEVELTITLE
+            </span>
+            </option>
+          </select>
+        </div>
+        &nbsp;&nbsp;<span i18n:translate="">in session</span>&nbsp;&nbsp;
+        <div class="form-group">
+          <select id="level_session" name="level_session" class="form-control">
+            <option tal:attributes="value session/code" tal:repeat="session view/session_dicts">
+              <span tal:replace="session/title">LEVELSESSION
+            </span>
+            </option>
+          </select>
+        </div>
+      </div>
+
+    </div>
+  </div>
+</form>  
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 16045)
@@ -0,0 +1,86 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <thead>
+  </thead>
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <span tal:content="widget/label">label</span>:
+        </td>
+        <td tal:on-error="default">
+          <div tal:content="structure widget">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+    </tal:block>
+      <tr>
+        <td i18n:translate="">
+            Study Mode:
+        </td>
+        <td>
+          <div tal:content="view/current_mode">
+          CURRENTMODE
+          </div>
+        </td>
+      </tr>
+      <tr tal:condition="python: view.department is not None">
+        <td i18n:translate="">
+            Department:
+        </td>
+        <td>
+          <a tal:attributes="href python: view.url(view.department)"
+               tal:content="view/department/longtitle">
+            DEPARTMENT
+          </a>
+        </td>
+      </tr>
+      <tr tal:condition="python: view.faculty is not None">
+        <td i18n:translate="">
+            Faculty:
+        </td>
+        <td>
+          <a tal:attributes="href python: view.url(view.faculty)" 
+               tal:content="view/faculty/longtitle">
+            FACULTY
+          </a>
+        </td>
+      </tr>
+      <tr tal:condition="view/prev_studycourses">
+        <td i18n:translate="">
+            Previous Study Courses:
+        </td>
+        <td>
+          <span tal:repeat="value view/prev_studycourses">
+            <a tal:attributes="href value/href" tal:content="value/title">
+              FACULTY
+            </a>
+          </span>
+        </td>
+      </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Study Levels (Course Lists)</h3>
+
+<table i18n:domain="waeup.kofa" class="table">
+  <thead>
+    <tr>
+      <th i18n:translate="">Level Code</th>
+      <th i18n:translate="">Level Title</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href python:view.url(value)">
+      <span tal:content="value/level">CODE</span></a></td>
+      <td tal:content="value/level_title">TITLE</td>
+    </tr>
+  </tbody>
+</table>
+
+
+<div tal:condition="python: not len(context.keys())"
+    i18n:domain="waeup.kofa" i18n:translate="">
+  There are no levels registered yet.
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt	(revision 16045)
@@ -0,0 +1,21 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <p i18n:translate="">In Kofa a study level is a container for courses,
+  which means it can list the courses taken by the student at a certain level.
+  The container also
+  stores and provides the information when and by whom the course list was
+  validated, in which session the level was taken and which verdict the
+  student finally obtained.
+  </p>
+
+  <div class="actionButtons" tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 16045)
@@ -0,0 +1,73 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <h3 i18n:translate="">
+    Course Tickets
+    (Total Credits:
+    <span i18n:name="total_credits"
+      tal:replace="view/context/total_credits">TOTAL</span>)
+  </h3>
+  <table class="kofa-data-table dataTableManage">
+  <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th i18n:translate="">Term</th>
+      <th i18n:translate="">Code</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Department</th>
+      <th i18n:translate="">Faculty</th>
+      <th i18n:translate="">Credits</th>
+      <th i18n:translate="">Score</th>
+      <th i18n:translate="">CO</th>
+      <th i18n:translate="">OS</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value view/translated_values">
+       <td>
+        <input type="checkbox" name="val_id"
+  		  tal:attributes="value value/__name__"
+        tal:condition="value/removable_by_student" />
+      </td>
+      <td tal:content="value/semester" nowrap>TERM</td>
+      <td> <a tal:attributes="href value/url">
+      <span tal:content="value/code">CODE</span>
+      </a></td>
+      <td tal:content="value/title">TITLE</td>
+      <td tal:content="value/dcode">DEPARTMENT</td>
+      <td tal:content="value/fcode">FACULTY</td>
+      <td tal:content="value/credits">CREDITS</td>
+      <td tal:content="value/score|nothing">SCORE</td>
+      <td tal:content="value/carry_over">CO</td>
+      <td tal:content="value/outstanding">OS</td>
+    </tr>
+  </tbody>
+  </table>
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+  <br /><br /><br />
+  <div class="form-inline">
+    <div class="form-group">
+        <input class="btn btn-primary" type="submit" name="ADD"
+               i18n:translate=""
+               value="Add course ticket" />
+    </div>
+    <div class="form-group">
+        <input type="text" name="course" class="form-control"
+               tal:attributes="placeholder view/placeholder" />
+    </div>
+  </div>
+  <br />
+  <p i18n:translate="">
+    You don't know the course code? Then select a course
+    <a href="ctadd">here</a>.
+  </p>
+</form>  
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 16045)
@@ -0,0 +1,106 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Study Level Data</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Course Tickets</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+      <tbody>
+        <tal:widgets content="structure provider:widgets" />
+      </tbody>
+      </table>
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.taboneactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br /><br />
+      <table class="kofa-data-table dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th>
+            <th i18n:translate="">Term</th>
+            <th i18n:translate="">Code</th>
+            <th i18n:translate="">Title</th>
+            <th i18n:translate="">Dept.</th>
+            <th i18n:translate="">Fact.</th>
+            <th i18n:translate="">Cred.</th>
+            <th i18n:translate="">Requ.</th>
+            <th i18n:translate="">Score</th>
+            <th i18n:translate="">CO</th>
+            <th i18n:translate="">OS</th>
+            <th i18n:translate="">Auto</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="value view/translated_values">
+            <td>
+              <input type="checkbox" name="val_id"
+        		  tal:attributes="value value/__name__" />
+            </td>
+            <td tal:content="value/semester" nowrap>TERM</td>
+            <td> <a tal:attributes="href value/url">
+            <span tal:content="value/code">CODE</span>
+            </a></td>
+            <td tal:content="value/title">TITLE</td>
+            <td tal:content="value/dcode">DEPARTMENT</td>
+            <td tal:content="value/fcode">FACULTY</td>
+            <td tal:content="value/credits">CREDITS</td>
+            <td tal:content="value/mandatory">MANDATORY</td>
+            <td tal:content="value/score|nothing">SCORE</td>
+            <td tal:content="value/carry_over">CO</td>
+            <td tal:content="value/outstanding">OS</td>
+            <td tal:content="value/automatic">AUTO</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:condition="python:action.label in view.tabtwoactions"
+                 tal:replace="structure action/render"/>
+        </span>
+      </div>
+      <br /><br /><br />
+      <div class="form-inline">
+        <div class="form-group">
+            <input class="btn btn-primary" type="submit" name="ADD"
+                   i18n:translate=""
+                   value="Add course ticket" />
+        </div>
+        <div class="form-group">
+            <input type="text" name="course" class="form-control"
+                   tal:attributes="placeholder view/placeholder" />
+        </div>
+      </div>
+      <br />
+      <p i18n:translate="">
+        You don't know the course code? Then select a course
+        <a href="add">here</a>.
+      </p>
+
+    </div>
+  </div>
+</form>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 16045)
@@ -0,0 +1,67 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <span tal:content="widget/label">label</span>:
+        </td>
+        <td tal:on-error="default">
+          <div tal:content="structure widget">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+    </tal:block>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Course Tickets</h3>
+
+<table class="kofa-data-table dataTable" i18n:domain="waeup.kofa">
+  <thead>
+    <tr>
+      <th i18n:translate="">Term</th>
+      <th i18n:translate="">Code</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Dep.</th>
+      <th i18n:translate="">Fac.</th>
+      <th i18n:translate="">Cred.</th>
+      <th i18n:translate="">Requ.</th>
+      <th i18n:translate="">Score</th>
+      <th i18n:translate="">Grade</th>
+      <th i18n:translate="">CO</th>
+      <th i18n:translate="">OS</th>
+      <th i18n:translate="">Auto</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value view/translated_values">
+      <td tal:content="value/semester" nowrap>TERM</td>
+      <td> <a tal:attributes="href value/url">
+      <span tal:content="value/code">CODE</span>
+      </a></td>
+      <td tal:content="value/title">TITLE</td>
+      <td tal:content="value/dcode">DEPARTMENT</td>
+      <td tal:content="value/fcode">FACULTY</td>
+      <td tal:content="value/credits">CREDITS</td>
+      <td tal:content="value/mandatory">MANDATORY</td>
+      <td tal:content="value/score|nothing">SCORE</td>
+      <td tal:content="value/grade|nothing">GRADE</td>
+      <td tal:content="value/carry_over">CO</td>
+      <td tal:content="value/outstanding">OS</td>
+      <td tal:content="value/automatic">AUTO</td>
+    </tr>
+  </tbody>
+</table>
+
+<p i18n:translate="">
+  CO: Carry-over course<br />
+  OS: Outstanding course<br />
+  Requ.: Required course<br />
+  Auto: Automatically created course ticket
+</p>
+
+<div i18n:domain="waeup.kofa"
+  tal:condition="python: not len(context.keys())" i18n:translate="">
+  The're no tickets added yet.
+</div>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelremarkpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelremarkpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/studylevelremarkpage.pt	(revision 16045)
@@ -0,0 +1,84 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Study Level Data</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">Course Tickets</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+      <br />
+      <table class="form-table">
+      <tbody>
+        <tal:widgets content="structure provider:widgets" />
+      </tbody>
+      </table>
+      <br />
+      <div tal:condition="view/availableActions">
+        <span tal:repeat="action view/actions"
+              tal:omit-tag="">
+          <input tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br /><br />
+      <table class="kofa-data-table dataTable" i18n:domain="waeup.kofa">
+        <thead>
+          <tr>
+            <th i18n:translate="">Term</th>
+            <th i18n:translate="">Code</th>
+            <th i18n:translate="">Title</th>
+            <th i18n:translate="">Dep.</th>
+            <th i18n:translate="">Fac.</th>
+            <th i18n:translate="">Cred.</th>
+            <th i18n:translate="">Requ.</th>
+            <th i18n:translate="">Score</th>
+            <th i18n:translate="">Grade</th>
+            <th i18n:translate="">CO</th>
+            <th i18n:translate="">OS</th>
+            <th i18n:translate="">Auto</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="value view/translated_values">
+            <td tal:content="value/semester" nowrap>TERM</td>
+            <td> <a tal:attributes="href value/url">
+            <span tal:content="value/code">CODE</span>
+            </a></td>
+            <td tal:content="value/title">TITLE</td>
+            <td tal:content="value/dcode">DEPARTMENT</td>
+            <td tal:content="value/fcode">FACULTY</td>
+            <td tal:content="value/credits">CREDITS</td>
+            <td tal:content="value/mandatory">MANDATORY</td>
+            <td tal:content="value/score|nothing">SCORE</td>
+            <td tal:content="value/grade|nothing">GRADE</td>
+            <td tal:content="value/carry_over">CO</td>
+            <td tal:content="value/outstanding">OS</td>
+            <td tal:content="value/automatic">AUTO</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <p i18n:translate="">
+        CO: Carry-over course<br />
+        OS: Outstanding course<br />
+        Requ.: Required course<br />
+        Auto: Automatically created course ticket
+      </p>
+    </div>
+  </div>
+</form>
+
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcript.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcript.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcript.pt	(revision 16045)
@@ -0,0 +1,62 @@
+<tal:def define="tdata python:context.getTranscriptData()">
+<table class="form-table" i18n:domain="waeup.kofa">
+  <thead>
+  </thead>
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td>
+          <span tal:content="widget/label">label</span>:
+        </td>
+        <td tal:on-error="default">
+          <div tal:content="structure widget">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <td>
+        <span i18n:translate="">Cumulative GPA</span>:
+      </td>
+      <td>
+        <div tal:content="python:'%.3f' % tdata[1]">GPA</div>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Subjects/Courses</h3>
+
+<table i18n:domain="waeup.kofa" class="form-table" >
+  <thead>
+    <tr>
+      <th i18n:translate="">Level</th>
+      <th i18n:translate="">Session</th>
+      <th i18n:translate="">Term</th>
+      <th i18n:translate="">Course</th>
+      <th i18n:translate="">Title</th>
+      <th i18n:translate="">Credits</th>
+      <th i18n:translate="">Grade</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tal:levels tal:repeat="level python:tdata[0]">
+      <tal:semesters tal:repeat="semester_tickets python:(level['tickets_1'],
+                                      level['tickets_2'], level['tickets_3'])">
+      <tr tal:repeat="ticket semester_tickets">
+        <td tal:content="python: view.level_dict.get('ticket.level', ticket.level)">LEVEL</td>
+        <td tal:content="python: view.session_dict[ticket.level_session]">SESSION</td>
+        <td tal:content="python: view.semester_dict[ticket.semester]">SEMESTER</td>
+        <td tal:content="ticket/code">CODE</td>
+        <td tal:content="ticket/title">TITLE</td>
+        <td tal:content="ticket/credits">CREDITS</td>
+        <td tal:content="ticket/grade|nothing">GRADE</td>
+      </tr>
+      </tal:semesters>
+    </tal:levels>
+  </tbody>
+</table>
+<br />
+<div tal:content="python:getattr(context, 'transcript_signees', '')">SIGNEES</div>
+</tal:def>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptprocess.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptprocess.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptprocess.pt	(revision 16045)
@@ -0,0 +1,68 @@
+<form method="POST" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Process request</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">View correspondence</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab3" data-toggle="tab">
+        <span i18n:translate="">View signatures</span>
+      </a>
+    </li>
+    <li>
+      <a href="#tab4" data-toggle="tab">
+        <span i18n:translate="">View remarks</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+
+      <table class="form-table">
+        <tbody>
+          <tr>
+            <td class="fieldname" i18n:translate="">
+            Comment:
+            </td>
+            <td>
+            <textarea cols="60" name="comment" rows="6"></textarea>
+            </td>
+          </tr>
+          <tr>
+            <td colspan="2">
+              <br />
+              <input type="submit" name="SUBMIT" class="btn btn-primary"
+                     tal:attributes="value view/buttonname" />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br /><p tal:content="structure view/correspondence"></p>
+    </div>
+
+    <div id="tab3" class="tab-pane">
+      <br /><p tal:content="structure view/signees"></p>
+    </div>
+
+    <div id="tab4" class="tab-pane">
+      <br />
+      <table class="table-condensed">
+        <tbody>
+          <span tal:omit-tag="" tal:content="structure view/remarks"></span>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptrequest.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptrequest.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/transcriptrequest.pt	(revision 16045)
@@ -0,0 +1,79 @@
+<form method="POST" i18n:domain="waeup.kofa">
+
+  <ul class="tabs nav nav-tabs" data-tabs="tabs">
+    <li class="active">
+      <a href="#tab1" data-toggle="tab">
+        <span i18n:translate="">Send new request</span>
+      </a></li>
+    <li>
+      <a href="#tab2" data-toggle="tab">
+        <span i18n:translate="">View correspondence</span>
+      </a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab1" class="active tab-pane">
+
+      <p i18n:translate="transcript_request"></p>
+
+      <table class="form-table" tal:condition="view/with_ac">
+        <tbody>
+          <tr>
+            <td i18n:translate="" class="fieldname">
+              Cover Letter:
+            </td>
+            <td>
+              <textarea cols="60" name="comment" rows="6"></textarea>
+            </td>
+          </tr>
+          <tr>
+            <td i18n:translate="" class="fieldname">
+              Dispatch Address:
+            </td>
+            <td>
+              <textarea cols="60" name="address" rows="6"></textarea>
+            </td>
+          </tr>
+          <tr>
+            <td i18n:translate="">
+              Activation Code:
+            </td>
+            <td>
+              <span tal:replace="view/ac_prefix">PIN Prefix</span> -
+              <input name="ac_series" type="text" maxlength="3"
+                     class="ac-input ac-series"
+                     value="" tal:attributes="value view/ac_series"/> -
+              <input name="ac_number" type="text" maxlength="10"
+                     class="ac-input ac-number"
+                     value="" tal:attributes="value view/ac_number" />
+              <font color="red"><span tal:replace="view/notice">Notice</span>
+              </font>
+            </td>
+          </tr>
+          <tr>
+            <td>&nbsp;</td>
+            <td i18n:translate="">
+            The activation code can be found on the payment slip.
+            </td>
+          </tr>
+          <tr>
+            <td colspan="2">
+              <input type="submit" name="SUBMIT" class="btn btn-primary"
+                     tal:attributes="value view/buttonname" />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <span tal:omit-tag="" tal:condition="not: view/with_ac">
+        <input type="submit" name="SUBMIT" class="btn btn-primary"
+               tal:attributes="value view/buttonname" />
+      </span>
+    </div>
+
+    <div id="tab2" class="tab-pane">
+      <br /><p tal:content="structure view/correspondence"></p>
+    </div>
+  </div>
+
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/trigtrans.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/trigtrans.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser_templates/trigtrans.pt	(revision 16045)
@@ -0,0 +1,19 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+    <br />
+
+    <select id="transition" name="transition" class="form-control half">
+      <option tal:repeat="transition view/getTransitions"
+              tal:attributes="value transition/name">
+        <span tal:replace="transition/title">TRANSITIONTITLE</span>
+      </option>
+    </select>
+    <br /><br />
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/catalog.py	(revision 16045)
@@ -0,0 +1,151 @@
+## $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
+##
+"""Cataloging and searching components for students.
+"""
+import grok
+from grok import index
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.kofa.catalog import FilteredCatalogQueryBase
+from waeup.kofa.interfaces import (
+    IUniversity, IQueryResultItem, academic_sessions_vocab)
+from waeup.kofa.students.interfaces import IStudent, ICourseTicket
+from waeup.kofa.university.vocabularies import course_levels
+from waeup.kofa.students.workflow import TRANSREQ, TRANSVAL, GRADUATED
+
+class StudentsCatalog(grok.Indexes):
+    """A catalog for students.
+    """
+    grok.site(IUniversity)
+    grok.name('students_catalog')
+    grok.context(IStudent)
+
+    student_id = index.Field(attribute='student_id')
+    fullname = index.Text(attribute='fullname')
+    email = index.Field(attribute='email')
+    reg_number = index.Field(attribute='reg_number')
+    matric_number = index.Field(attribute='matric_number')
+    state = index.Field(attribute='state')
+    certcode = index.Field(attribute='certcode')
+    depcode = index.Field(attribute='depcode')
+    faccode = index.Field(attribute='faccode')
+    current_session = index.Field(attribute='current_session')
+    current_level = index.Field(attribute='current_level')
+    current_mode = index.Field(attribute='current_mode')
+
+class StudentQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    title = u'Student Query Item'
+    description = u'Some student found in a search'
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.student_id = context.student_id
+        self.display_fullname = context.display_fullname
+        self.reg_number = context.reg_number
+        self.matric_number = context.matric_number
+        self.state = context.state
+        self.translated_state = context.translated_state
+        self.current_level = context['studycourse'].current_level
+        try:
+            current_session = academic_sessions_vocab.getTerm(
+                context['studycourse'].current_session).title
+        except LookupError:
+            current_session = None
+        self.current_session = current_session
+        self.certificate = context['studycourse'].certificate
+        self.comment = getattr(context, 'officer_comment', None)
+
+def search(query=None, searchtype=None, view=None):
+    hitlist = []
+    if searchtype in ('fullname',):
+        results = Query().searchResults(
+            Text(('students_catalog', searchtype), query))
+    elif searchtype == 'suspended':
+        # 'suspended' is not indexed
+        cat = queryUtility(ICatalog, name='students_catalog')
+        all = cat.searchResults(student_id=(None, None))
+        for student in all:
+            if student.suspended:
+                hitlist.append(StudentQueryResultItem(student, view=view))
+        return hitlist
+    elif searchtype in (TRANSREQ, TRANSVAL, GRADUATED):
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(state=(searchtype, searchtype))
+    else:
+        # Temporary solution to display all students added
+        if query == '*':
+            cat = queryUtility(ICatalog, name='students_catalog')
+            results = cat.searchResults(student_id=(None, None))
+        else:
+            results = Query().searchResults(
+                Eq(('students_catalog', searchtype), query))
+    for result in results:
+        hitlist.append(StudentQueryResultItem(result, view=view))
+    return hitlist
+
+class SimpleFieldSearch(object):
+    """A programmatic (no UI required) search.
+
+    Looks up a given field attribute of the students catalog for a
+    single value. So normally you would call an instance of this
+    search like this:
+
+      >>> SimpleFieldSearch()(reg_number='somevalue')
+
+    """
+    catalog_name = 'students_catalog'
+    def __call__(self, **kw):
+        """Search students catalog programmatically.
+        """
+        if len(kw) != 1:
+            raise ValueError('must give exactly one index name to search')
+        cat = queryUtility(ICatalog, name=self.catalog_name)
+        index_name, query_term = kw.items()[0]
+        results = cat.searchResults(index_name=(query_term, query_term))
+        return results
+
+#: an instance of `SimpleFieldSearch` looking up students catalog.
+simple_search = SimpleFieldSearch()
+
+class CourseTicketIndexes(grok.Indexes):
+    """A catalog for course tickets.
+    """
+    grok.site(IUniversity)
+    grok.name('coursetickets_catalog')
+    grok.context(ICourseTicket)
+
+    level = index.Field(attribute='level')
+    session = index.Field(attribute='level_session')
+    code = index.Field(attribute='code')
+
+class StudentsQuery(FilteredCatalogQueryBase):
+    """Query students in a site. See waeup.kofa.catalog for more info.
+    """
+    cat_name = 'students_catalog'
+    defaults = dict(student_id=None) # make sure we get all studs by default
+
+class CourseTicketsQuery(FilteredCatalogQueryBase):
+    """Query students in a site. See waeup.kofa.catalog for more info.
+    """
+    cat_name = 'coursetickets_catalog'
+    defaults = dict(code=None) # make sure we get all tickets by default
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/container.py	(revision 16045)
@@ -0,0 +1,76 @@
+## $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
+##
+"""
+Containers for students.
+"""
+import grok
+from thread import allocate_lock
+from transaction import commit
+from zope.component import getUtility
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentsUtils)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.utils.logger import Logger
+
+lock = allocate_lock() # a lock object to lock threads.
+
+class StudentsContainer(grok.Container, Logger):
+    """
+    The node containing the student models
+    """
+
+    grok.implements(IStudentsContainer)
+
+    _curr_stud_id = 10 ** 6
+
+    logger_name = 'waeup.kofa.${sitename}.students'
+    logger_filename = 'students.log'
+
+    @property
+    def unique_student_id(self):
+        """A unique student id.
+
+        The student id returned is guaranteed to be unique. It
+        consists of some prefix (normally a single letter) followed by
+        a number with at least 7 digits.
+
+        Once a student id was issued, it won't be issued again.
+
+        Obtaining a student id is currently not thread-safe but can be
+        made easily by enabling commented lines.
+        """
+        prefix = getUtility(IStudentsUtils).STUDENT_ID_PREFIX
+
+        # lock.acquire() # lock data
+        new_id = u'%s%s' % (prefix, self._curr_stud_id)
+        self._curr_stud_id += 1
+        # self._p_changed = True
+        # commit()
+        # lock.release() # end of lock
+        return new_id
+
+    def addStudent(self, student):
+        """Add a student with subcontainers.
+        """
+        if not IStudent.providedBy(student):
+            raise TypeError(
+                'StudentsContainers contain only IStudent instances')
+        self[student.student_id] = student
+        return
+
+StudentsContainer = attrs_to_fields(StudentsContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/dynamicroles.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/dynamicroles.py	(revision 16045)
@@ -0,0 +1,119 @@
+## $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
+##
+"""Security policy components for students.
+
+Students need special security policy treatment, as officers with
+local roles for departments and faculties might have additional
+permissions (local roles on depts/faculties) here.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.principalpermission import (
+    AnnotationPrincipalPermissionManager,)
+from zope.securitypolicy.principalrole import AnnotationPrincipalRoleManager
+from waeup.kofa.students.interfaces import IStudent
+
+class StudentPrincipalRoleManager(AnnotationPrincipalRoleManager,
+                                    grok.Adapter):
+    grok.provides(IPrincipalRoleManager)
+    grok.context(IStudent)
+
+    #: The attribute name to lookup for additional roles
+    extra_attrib = 'certificate'
+    subcontainer = 'studycourse'
+
+    # Role name mapping:
+    # role name to look for in `extra_attrib` and parents
+    # to
+    # role to add in case this role was found
+    rolename_mapping = {
+        'waeup.local.ClearanceOfficer': 'waeup.StudentsClearanceOfficer',
+        'waeup.local.LocalStudentsManager': 'waeup.StudentsManager',
+        'waeup.local.LocalWorkflowManager': 'waeup.WorkflowManager',
+        'waeup.local.TranscriptOfficer': 'waeup.TranscriptOfficer',
+        'waeup.local.TranscriptSignee': 'waeup.TranscriptSignee',
+        'waeup.local.ReportsOfficer': 'waeup.ReportsOfficer',
+        }
+
+    def getRolesForPrincipal(self, principal_id):
+        """Get roles for principal with id `principal_id`.
+
+        See waeup.kofa.applicants.dynamicroles.ApplicantPrincipalRoleManager
+        for further information.
+        """
+        apr_manager = AnnotationPrincipalRoleManager(self._context)
+        result = apr_manager.getRolesForPrincipal(principal_id)
+        if result != []:
+            # If there are local roles defined here, no additional
+            # lookup is done.
+            return result
+        # The principal has no local roles yet. Let's lookup the
+        # connected course, dept, etc.
+        if self.subcontainer:
+            obj = getattr(
+                self._context[self.subcontainer], self.extra_attrib, None)
+            current_level = getattr(
+                self._context[self.subcontainer], 'current_level', 0)
+        else:
+            obj = getattr(self._context, self.extra_attrib, None)
+            current_level = 0
+        # Lookup local roles for connected certificate and all parent
+        # objects. This way we fake 'role inheritance'.
+        while obj is not None:
+            extra_roles = IPrincipalRoleManager(obj).getRolesForPrincipal(
+                principal_id)
+            for role_id, setting in extra_roles:
+                if 'CourseAdviser' in role_id:
+                    # Found a Course Adviser role in external attribute or parent
+                    # thereof. We need a special treatment for Course Advisers.
+                    if current_level and str(100*(current_level/100)) in role_id:
+                        # Grant additional role, which allows to validate or reject
+                        # course lists, only if external role corresponds
+                        # with current_level of student.
+                        result.append(
+                            ('waeup.StudentsCourseAdviser', setting))
+                    else:
+                        # Otherwise grant at least view permissions.
+                        result.append(
+                            ('waeup.StudentsOfficer', setting))
+                elif 'UGClearanceOfficer' in role_id:
+                    if not self._context.is_postgrad:
+                        result.append(
+                            ('waeup.StudentsClearanceOfficer', setting))
+                    else:
+                        # Otherwise grant at least view permissions.
+                        result.append(
+                            ('waeup.StudentsOfficer', setting))
+                elif 'PGClearanceOfficer' in role_id:
+                    if self._context.is_postgrad:
+                        result.append(
+                            ('waeup.StudentsClearanceOfficer', setting))
+                    else:
+                        # Otherwise grant at least view permissions.
+                        result.append(
+                            ('waeup.StudentsOfficer', setting))
+                elif role_id in self.rolename_mapping.keys():
+                    # Grant additional role
+                    # permissions (allow, deny or unset)
+                    # according to the rolename mapping above.
+                    result.append(
+                        (self.rolename_mapping[role_id], setting))
+                    # Local roles have been found, no need to climb up further.
+                    obj = None
+            obj = getattr(obj, '__parent__', None)
+        return result
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/export.py	(revision 16045)
@@ -0,0 +1,931 @@
+## $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
+##
+"""Exporters for student related stuff.
+"""
+import os
+import grok
+from datetime import datetime, timedelta
+from zope.component import getUtility
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.university.interfaces import ICertificateCourse, ICourse
+from waeup.kofa.students.catalog import StudentsQuery, CourseTicketsQuery
+from waeup.kofa.students.interfaces import (
+    IStudent, IStudentStudyCourse, IStudentStudyLevel, ICourseTicket,
+    IStudentOnlinePayment, ICSVStudentExporter, IBedTicket)
+from waeup.kofa.students.vocabularies import study_levels
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.utils.helpers import iface_names, to_timezone
+
+
+def get_students(site, stud_filter=StudentsQuery()):
+    """Get all students registered in catalog in `site`.
+    """
+    return stud_filter.query()
+
+def get_studycourses(students):
+    """Get studycourses of `students`.
+    """
+    return [x.get('studycourse', None) for x in students
+            if x is not None]
+
+def get_levels(students, **kw):
+    """Get all studylevels of `students`.
+    """
+    levels = []
+    level_session = kw.get('level_session', None)
+    for course in get_studycourses(students):
+        for level in course.values():
+            if level_session not in ('all', None) and \
+                int(level_session) != level.level_session:
+                continue
+            levels.append(level)
+    return levels
+
+def get_tickets(students, **kw):
+    """Get course tickets of `students`.
+    If code is passed through, filter course tickets
+    which belong to this course code and meet level=level
+    and level_session=level_session.
+    If not, but ct_level, ct_session and ct_semester
+    are passed through, filter course tickets
+    which meet level==ct_level, level_session==ct_session
+    and semester==ct_semester.
+    """
+    tickets = []
+    code = kw.get('code', None)
+    level = kw.get('level', None)
+    session = kw.get('session', None)
+    ct_level = kw.get('ct_level', None)
+    ct_session = kw.get('ct_session', None)
+    ct_semester = kw.get('ct_semester', None)
+    if code is None:
+        for level_obj in get_levels(students, **kw):
+            for ticket in level_obj.values():
+                if ct_level not in ('all', None):
+                    if level_obj.level in (10, 999, None)  \
+                        and int(ct_level) != level_obj.level:
+                        continue
+                    if level_obj.level not in range(
+                        int(ct_level), int(ct_level)+100, 10):
+                        continue
+                if ct_session not in ('all', None) and \
+                    int(ct_session) != level_obj.level_session:
+                    continue
+                if ct_semester not in ('all', None) and \
+                    int(ct_semester) != ticket.semester:
+                    continue
+                tickets.append(ticket)
+    else:
+        for level_obj in get_levels(students, **kw):
+            for ticket in level_obj.values():
+                if ticket.code != code:
+                    continue
+                if level not in ('all', None):
+                    if level_obj.level in (10, 999, None)  \
+                        and int(level) != level_obj.level:
+                        continue
+                    if level_obj.level not in range(
+                        int(level), int(level)+100, 10):
+                        continue
+                if session not in ('all', None) and \
+                    int(session) != level_obj.level_session:
+                    continue
+                tickets.append(ticket)
+    return tickets
+
+def get_tickets_for_lecturer(students, **kw):
+    """Get course tickets of `students`.
+    Filter course tickets which belong to this course code and
+    which are editable by lecturers.
+    """
+    tickets = []
+    code = kw.get('code', None)
+    level_session = kw.get('session', None)
+    level = kw.get('level', None)
+    for level_obj in get_levels(students, **kw):
+        for ticket in level_obj.values():
+            if ticket.code != code:
+                continue
+            if not ticket.editable_by_lecturer:
+                continue
+            if level not in ('all', None):
+                if level_obj.level in (10, 999, None)  \
+                    and int(level) != level_obj.level:
+                    continue
+                if level_obj.level not in range(int(level), int(level)+100, 10):
+                    continue
+            if level_session not in ('all', None) and \
+                int(level_session) != level_obj.level_session:
+                continue
+            tickets.append(ticket)
+    return tickets
+
+def get_outstanding(students, **kw):
+    """Get students with outstanding certificate courses.
+    """
+    students_wo = []
+    for student in students:
+        certificate = getattr(
+            student.get('studycourse', None), 'certificate', None)
+        if certificate:
+            allticketcodes = []
+            failedticketcodes = '' # taken but failed
+            nottakenticketcodes = '' # registered but not taken
+            missedticketcodes = '' # not registered
+            for level in student['studycourse'].values():
+                failedticketcodes += level.passed_params[4]
+                nottakenticketcodes += level.passed_params[5]
+                for ticket in level.values():
+                    allticketcodes.append(ticket.code)
+            for certcourse in certificate.values():
+                if certcourse.getCourseCode() not in allticketcodes:
+                    missedticketcodes += '%s ' % certcourse.__name__
+            student_wo = (student, missedticketcodes,
+                          failedticketcodes, nottakenticketcodes)
+            students_wo.append(student_wo)
+    return students_wo
+
+def get_payments(students, p_states=None, **kw):
+    """Get all payment tickets of `students` within given payment_date period.
+    """
+    date_format = '%d/%m/%Y'
+    payments = []
+    p_start = kw.get('payments_start')
+    p_end = kw.get('payments_end')
+    paycat = kw.get('paycat')
+    paysession = kw.get('paysession')
+    for student in students:
+        for payment in student.get('payments', {}).values():
+            if p_start and p_end:
+                if not payment.payment_date:
+                    continue
+                payments_start = datetime.strptime(p_start, date_format)
+                payments_end = datetime.strptime(p_end, date_format)
+                tz = getUtility(IKofaUtils).tzinfo
+                payments_start = tz.localize(payments_start)
+                payments_end = tz.localize(payments_end) + timedelta(days=1)
+                payment_date = to_timezone(payment.payment_date, tz)
+                if payment_date < payments_start or payment_date > payments_end:
+                    continue
+            if p_states and not payment.p_state in p_states:
+                continue
+            if paycat not in ('all', None) and payment.p_category != paycat:
+                continue
+            if paysession not in ('all', None) \
+                and payment.p_session != int(paysession):
+                continue
+            payments.append(payment)
+    return payments
+
+def get_bedtickets(students):
+    """Get all bed tickets of `students`.
+    """
+    tickets = []
+    for student in students:
+        for ticket in student.get('accommodation', {}).values():
+            tickets.append(ticket)
+    return tickets
+
+class StudentExporterBase(ExporterBase):
+    """Exporter for students or related objects.
+    This is a baseclass.
+    """
+    grok.baseclass()
+    grok.implements(ICSVStudentExporter)
+    grok.provides(ICSVStudentExporter)
+
+    def filter_func(self, x, **kw):
+        return x
+
+    def get_filtered(self, site, **kw):
+        """Get students from a catalog filtered by keywords.
+        students_catalog is the default catalog. The keys must be valid
+        catalog index names.
+        Returns a simple empty list, a list with `Student`
+        objects or a catalog result set with `Student`
+        objects.
+
+        .. seealso:: `waeup.kofa.students.catalog.StudentsCatalog`
+
+        """
+        # Pass only given keywords to create FilteredCatalogQuery objects.
+        # This way we avoid
+        # trouble with `None` value ambivalences and queries are also
+        # faster (normally less indexes to ask). Drawback is, that
+        # developers must look into catalog to see what keywords are
+        # valid.
+        if kw.get('catalog', None) == 'coursetickets':
+            coursetickets = CourseTicketsQuery(**kw).query()
+            students = []
+            for ticket in coursetickets:
+                students.append(ticket.student)
+            return list(set(students))
+        # Payments can be filtered by payment date and payment category.
+        # These parameters are not keys of the catalog and must thus be
+        # removed from kw.
+        try:
+            del kw['payments_start']
+            del kw['payments_end']
+            del kw['paycat']
+            del kw['paysession']
+        except KeyError:
+            pass
+        # Coursetickets can be filtered by level and session.
+        # These parameters are not keys of the catalog and must thus be
+        # removed from kw.
+        try:
+            del kw['ct_level']
+            del kw['ct_session']
+            del kw['ct_semester']
+        except KeyError:
+            pass
+        # Studylevels can be filtered by level_session.
+        # This parameter is not a key of the catalog and must thus be
+        # removed from kw.
+        try:
+            del kw['level_session']
+        except KeyError:
+            pass
+        query = StudentsQuery(**kw)
+        return query.query()
+
+    def get_selected(self, site, selected):
+        """Get set of selected students.
+        Returns a simple empty list or a list with `Student`
+        objects.
+        """
+        students = []
+        students_container = site.get('students', {})
+        for id in selected:
+            student = students_container.get(id, None)
+            if student is None:
+                # try matric number
+                result = list(StudentsQuery(matric_number=id).query())
+                if result:
+                    student = result[0]
+                else:
+                    continue
+            students.append(student)
+        return students
+
+    def export(self, values, filepath=None):
+        """Export `values`, an iterable, as CSV file.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for value in values:
+            self.write_item(value, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export students into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        return self.export(self.filter_func(get_students(site)), filepath)
+
+    def export_student(self, student, filepath=None):
+        return self.export(self.filter_func([student]), filepath=filepath)
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export items denoted by `kw`.
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        data = self.get_filtered(site, **kw)
+        return self.export(self.filter_func(data, **kw), filepath=filepath)
+
+    def export_selected(self,site, filepath=None, **kw):
+        """Export data for selected set of students.
+        """
+        selected = kw.get('selected', [])
+        data = self.get_selected(site, selected)
+        return self.export(self.filter_func(data, **kw), filepath=filepath)
+
+
+class StudentExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Exporter first filters the set of students by searching the
+    students catalog. Then it exports student base data of this set of students.
+    """
+    grok.name('students')
+
+    fields = tuple(sorted(iface_names(IStudent))) + (
+        'password', 'state', 'history', 'certcode', 'is_postgrad',
+        'current_level', 'current_session')
+    title = _(u'Students (Data Backup)')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler prepares the history messages and adds a hash symbol at
+        the end of the phone number to avoid annoying automatic number
+        transformation by Excel or Calc."""
+        if name == 'history':
+            value = value.messages
+        if 'phone' in name and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            StudentExporter, self).mangle_value(
+            value, name, context=context)
+
+class TrimmedDataExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Trimmed Data Exporter first filters the set of students 
+    by searching the students catalog. Then it exports a trimmed data set
+    of this set of students. 
+    """
+    grok.name('trimmed')
+
+    fields = (
+        'student_id',
+        'matric_number',
+        'reg_number',
+        'firstname',
+        'middlename',
+        'lastname',
+        'sex',
+        'email',
+        'phone',
+        'nationality',
+        'date_of_birth',
+        'state',
+        'current_mode',
+        'certcode',
+        'faccode',
+        'depcode',
+        'current_level',
+        'current_session',
+        'current_verdict')
+    title = _(u'Students (Trimmed Data)')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler prepares the history messages and adds a hash symbol at
+        the end of the phone number to avoid annoying automatic number
+        transformation by Excel or Calc."""
+        if 'phone' in name and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            TrimmedDataExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Study Course Exporter first filters the set of students
+    by searching the students catalog. Then it exports the data of the current
+    study course container of each student from this set. It does
+    not export their content.
+    """
+    grok.name('studentstudycourses')
+
+    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
+    title = _(u'Student Study Courses (Data Backup)')
+
+    def filter_func(self, x, **kw):
+        return get_studycourses(x)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the certificate code and the student id.
+        """
+        if name == 'certificate' and value is not None:
+            # XXX: hopefully cert codes are unique site-wide
+            value = value.code
+        if name == 'student_id' and context is not None:
+            student = context.student
+            value = getattr(student, name, None)
+        return super(
+            StudentStudyCourseExporter, self).mangle_value(
+            value, name, context=context)
+
+
+class StudentStudyLevelExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Study Level Exporter first filters the set of students
+    by searching the students catalog. Then it exports the data of the student's
+    study level container data but not their content (course tickets).
+    The exporter iterates over all objects in the students' ``studycourse``
+    containers.
+    """
+    grok.name('studentstudylevels')
+
+    fields = tuple(sorted(iface_names(
+        IStudentStudyLevel))) + (
+        'student_id', 'number_of_tickets','certcode')
+    title = _(u'Student Study Levels (Data Backup)')
+
+    def filter_func(self, x, **kw):
+        return get_levels(x, **kw)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student id, nothing else.
+        """
+        if name == 'student_id' and context is not None:
+            student = context.student
+            value = getattr(student, name, None)
+        return super(
+            StudentStudyLevelExporter, self).mangle_value(
+            value, name, context=context)
+
+class CourseTicketExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Course Ticket Exporter exports course tickets. Usually,
+    the exporter first filters the set of students by searching the
+    students catalog. Then it collects and iterates over all ``studylevel``
+    containers of the filtered student set and finally
+    iterates over all items inside these containers.
+
+    If the course code is passed through, the exporter uses a different
+    catalog. It searches for students in the course tickets catalog and
+    exports those course tickets which belong to the given course code and
+    also meet level and session passed through at the same time.
+    This happens if the exporter is called at course level in the academic
+    section.
+    """
+    grok.name('coursetickets')
+
+    fields = tuple(sorted(iface_names(ICourseTicket) +
+        ['level', 'code', 'level_session'])) + ('student_id',
+        'certcode', 'display_fullname')
+    title = _(u'Course Tickets (Data Backup)')
+
+    def filter_func(self, x, **kw):
+        return get_tickets(x, **kw)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student's id and fullname.
+        """
+        if context is not None:
+            student = context.student
+            if name in ('student_id', 'display_fullname') and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            CourseTicketExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudentPaymentExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Payment Exporter first filters the set of students
+    by searching the students catalog. Then it exports student payment
+    tickets by iterating over the items of the student's ``payments``
+    container. If the payment period is given, only tickets, which were
+    paid in payment period, are considered for export.
+    """
+    grok.name('studentpayments')
+
+    fields = tuple(
+        sorted(iface_names(
+            IStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item', 'certificate', 'student']))) + (
+            'student_id','state','current_session')
+    title = _(u'Student Payments (Data Backup)')
+
+    def filter_func(self, x, **kw):
+        return get_payments(x, **kw)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student's id, registration
+        state and current session.
+        """
+        if context is not None:
+            student = context.student
+            if name in ['student_id','state',
+                        'current_session'] and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            StudentPaymentExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudentTrimmedPaymentExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Trimmed Payment Exporter is a slightly customized version
+    of the StudentPaymentExporter requested by Uniben.
+    """
+    grok.name('trimmedpayments')
+
+    fields = tuple(
+        sorted(iface_names(
+            IStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item', 'certificate', 'student', 'ac']))) + (
+            'student_id','faccode', 'depcode', 'state','current_session')
+    title = _(u'Student Payments (Sorted Data)')
+
+    def filter_func(self, x, **kw):
+        payments = get_payments(x, **kw)
+        return sorted([payment for payment in payments],
+            key=lambda payment: str(payment.p_category) + str(payment.student.faccode)
+                + str(payment.student.depcode) + str(payment.p_item))
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student's id, registration
+        state and current session.
+        """
+        if context is not None:
+            student = context.student
+            if name in ['student_id','state', 'faccode', 'depcode',
+                        'current_session'] and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            StudentTrimmedPaymentExporter, self).mangle_value(
+            value, name, context=context)
+
+class DataForLecturerExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Data for Lecturer Exporter searches for students in the course
+    tickets catalog and exports those course tickets which belong to the
+    given course code, meet level and session passed through at the
+    same time, and which are editable by lecturers. This exporter can only
+    be called at course level in the academic section.
+    """
+    grok.name('lecturer')
+
+    fields = ('matric_number', 'student_id', 'display_fullname',
+              'level', 'code', 'level_session', 'score')
+
+    title = _(u'Data for Lecturer')
+
+    def filter_func(self, x, **kw):
+        tickets = get_tickets_for_lecturer(x, **kw)
+        return sorted([ticket for ticket in tickets],
+            key=lambda ticket: str(ticket.fcode) + str(ticket.dcode)
+                + str(ticket.student.matric_number))
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student's id and fullname.
+        """
+        if context is not None:
+            student = context.student
+            if name in ('matric_number',
+                        'reg_number',
+                        'student_id',
+                        'display_fullname',) and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            DataForLecturerExporter, self).mangle_value(
+            value, name, context=context)
+
+class OutstandingCoursesExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Student Outstanding Courses Exporter first filters the set of 
+    students by searching the students catalog. Then it exports students with 
+    lists of outstanding courses, i.e. courses which the student has
+    missed (not registered at all), failed (registered but not passed)
+    or nottaken (registered but not taken).
+    """
+    grok.name('outstandingcourses')
+
+    fields = ('student_id', 'matric_number', 'certcode', 'display_fullname',
+              'missed', 'failed', 'nottaken')
+    title = _(u'Outstanding Courses')
+
+    def filter_func(self, x, **kw):
+        return get_outstanding(x, **kw)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student's id, fullname and certcode,
+        and it collects the lists of outstanding courses.
+        """
+        if context is not None:
+            if name in ('student_id', 'matric_number',
+                        'display_fullname', 'certcode'):
+                value = getattr(context[0], name, None)
+            elif name == 'missed':
+                value = context[1]
+            elif name == 'failed':
+                value = context[2]
+            elif name == 'nottaken':
+                value = context[3]
+        return super(
+            OutstandingCoursesExporter, self).mangle_value(
+            value, name, context=context)
+
+class UnpaidPaymentsExporter(StudentPaymentExporter):
+    """The Unpaid Payments Exporter works just like the
+    Student Payment (singular intended) Exporter  but it exports only 
+    unpaid tickets. This exporter is designed for finding and finally 
+    purging outdated payment tickets.
+    """
+    grok.name('unpaidpayments')
+
+    title = _(u'Unpaid Payment Tickets')
+
+    def filter_func(self, x, **kw):
+        return get_payments(x, p_states=('unpaid',) , **kw)
+
+class DataForBursaryExporter(StudentPaymentExporter):
+    """The Data for Bursary Exporter works just like the Student Payment
+    Exporter but it exports much more information about the student. It combines
+    payment and student data in one table in order to spare postprocessing of 
+    two seperate export files. The exporter is primarily used by bursary
+    officers who have exclusively access to this exporter. The exporter
+    exports ``paid``, ``waived`` and ``scholarship`` payment tickets.
+    """
+    grok.name('bursary')
+
+    def filter_func(self, x, **kw):
+        return get_payments(x, p_states=('paid', 'waived', 'scholarship'), **kw)
+
+    fields = tuple(
+        sorted(iface_names(
+            IStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item', 'certificate', 'student']))) + (
+            'student_id','matric_number','reg_number',
+            'firstname', 'middlename', 'lastname',
+            'state','current_session',
+            'entry_session', 'entry_mode',
+            'faccode', 'depcode','certcode')
+    title = _(u'Payment Data for Bursary')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler fetches the student data.
+        """
+        if context is not None:
+            student = context.student
+            if name in [
+                'student_id','matric_number', 'reg_number',
+                'firstname', 'middlename', 'lastname',
+                'state', 'current_session',
+                'entry_session', 'entry_mode',
+                'faccode', 'depcode', 'certcode'] and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            StudentPaymentExporter, self).mangle_value(
+            value, name, context=context)
+
+class AccommodationPaymentsExporter(DataForBursaryExporter):
+    """The Accommodation Payments Exporter works like the Data for Bursary
+    Exporter above. The exporter exports ``paid``, ``waived`` and ``scholarship``
+    payment tickets with category ``bed_allocation`` or ``hostel_maintenance``.
+    The exporter is primarily used by accommodation officers who have
+    exclusively access to this exporter.
+    """
+    grok.name('accommodationpayments')
+
+    def filter_func(self, x, **kw):
+        kw['paycat'] = 'bed_allocation'
+        payments = get_payments(x, p_states=(
+          'paid', 'waived', 'scholarship'), **kw)
+        kw['paycat'] = 'hostel_maintenance'
+        payments += get_payments(x, p_states=(
+          'paid', 'waived', 'scholarship'), **kw)
+        return payments
+
+    title = _(u'Accommodation Payments')
+
+class BedTicketExporter(grok.GlobalUtility, StudentExporterBase):
+    """The Bed Ticket Exporter first filters the set of students
+    by searching the students catalog. Then it exports bed
+    tickets by iterating over the items of the student's ``accommodation``
+    container.
+    """
+    grok.name('bedtickets')
+
+    fields = tuple(
+        sorted(iface_names(
+            IBedTicket, exclude_attribs=False,
+            omit=['display_coordinates', 'maint_payment_made']))) + (
+            'student_id', 'actual_bed_type')
+    title = _(u'Bed Tickets (Data Backup)')
+
+    def filter_func(self, x, **kw):
+        return get_bedtickets(x)
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the student id and the type of the bed
+        which has been booked in the ticket.
+        """
+        if context is not None:
+            student = context.student
+            if name in ['student_id'] and student is not None:
+                value = getattr(student, name, None)
+        if name == 'bed' and value is not None:
+            value = getattr(value, 'bed_id', None)
+        if name == 'actual_bed_type':
+            value = getattr(getattr(context, 'bed', None), 'bed_type', None)
+        return super(
+            BedTicketExporter, self).mangle_value(
+            value, name, context=context)
+
+class SchoolFeePaymentsOverviewExporter(StudentExporter):
+    """The School Fee Payments Overview Exporter first filters the set of students
+    by searching the students catalog. Then it exports some student base data
+    together with the total school fee amount paid in each year over a
+    predefined year range (current year - 9, ... , current year + 1).
+    """
+    grok.name('sfpaymentsoverview')
+
+    curr_year = datetime.now().year
+    year_range = range(curr_year - 11, curr_year + 1)
+    year_range_tuple = tuple([str(year) for year in year_range])
+    fields = ('student_id', 'matric_number', 'display_fullname',
+        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
+        'current_level', 'current_session', 'current_mode',
+        'entry_session', 'reg_number'
+        ) + year_range_tuple
+    title = _(u'School Fee Payments Overview')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler summarizes the school fee amounts made per year. It
+        iterates over all paid school fee payment tickets and
+        adds together the amounts paid in a year. Waived payments
+        are marked ``waived`` and scholarship payments marked `scholarship`.
+        """
+        if name in self.year_range_tuple and context is not None:
+            value = 0
+            for ticket in context['payments'].values():
+                if ticket.p_category == 'schoolfee' and \
+                    ticket.p_session == int(name):
+                    if ticket.p_state == 'waived':
+                        value = 'waived'
+                        break
+                    if ticket.p_state == 'scholarship':
+                        value = 'scholarship'
+                        break
+                    if ticket.p_state == 'paid':
+                        try:
+                            value += ticket.amount_auth
+                        except TypeError:
+                            pass
+            if value == 0:
+                value = ''
+            elif isinstance(value, float):
+                value = round(value, 2)
+        return super(
+            StudentExporter, self).mangle_value(
+            value, name, context=context)
+
+class SessionPaymentsOverviewExporter(StudentExporter):
+    """The Session Payments Overview Exporter first filters the set of students
+    by searching the students catalog. Then it exports some student base data
+    together with the total amount paid in predefined payment categories
+    over the previous three session (referring to current academic session).
+    Sample output:
+
+    header: ``...schoolfee13,schoolfee14,schoolfee15,gown13,gown14,gown15...``
+
+    data: ``...2000.0,,3000.0,,,1000.0,...``
+
+    This csv data string means that the student paid 2000.0 school fee in 2013
+    and 3000.0 in 2015. S/He furthermore paid 1000.0 for gown rental in 2015.
+    """
+    grok.name('sessionpaymentsoverview')
+
+    paycats = ('schoolfee', 'clearance', 'gown', 'transcript')
+    regular_fields = ('student_id', 'matric_number', 'display_fullname',
+        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
+        'current_level', 'current_session', 'current_mode',
+        'entry_session', 'reg_number'
+        )
+    title = _(u'Session Payments Overview')
+
+    @property
+    def paycatyears(self):
+        cas = grok.getSite()['configuration'].current_academic_session
+        paycatyears = []
+        if cas:
+            year_range = range(cas-2, cas+1)
+            year_range_tuple = tuple([str(year)[2:] for year in year_range])
+            paycatyears = [
+                cat+year for cat in self.paycats for year in year_range_tuple]
+        return paycatyears
+
+    @property
+    def fields(self):
+        return self.regular_fields + tuple(self.paycatyears)
+
+    def mangle_value(self, value, name, context=None):
+        """
+        """
+        amounts = dict()
+        for catyear in self.paycatyears:
+            amounts[catyear] = 0.0
+        if name[:-2] in self.paycats and context is not None:
+            for ticket in context['payments'].values():
+                if ticket.p_category == name[:-2]:
+                    if ticket.p_state in ('waived', 'paid'):
+                        if str(ticket.p_session)[2:] == name[-2:]:
+                            amounts[name] += ticket.amount_auth
+            if amounts[name] == 0.0:
+                value = ''
+            elif isinstance(amounts[name], float):
+                value = round(amounts[name], 2)
+        return super(
+            StudentExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudyLevelsOverviewExporter(StudentExporter):
+    """The Student Study Levels Overview Exporter first filters the set of
+    students by searching the students catalog. Then it exports some student
+    base data together with the session key of registered levels.
+    Sample output:
+
+    header: ``...100,110,120,200,210,220,300...``
+
+    data: ``...2010,,,2011,2012,,2013...``
+
+    This csv data string means that level 100 was registered in session 
+    2010/2011, level 200 in session 2011/2012, level 210 (200 on 1st probation)
+    in session 2012/2013 and level 300 in session 2013/2014.
+    """
+    grok.name('studylevelsoverview')
+
+    avail_levels = tuple([str(x) for x in study_levels(None)])
+
+    fields = ('student_id', ) + (
+        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
+        'entry_session', 'current_level', 'current_session',
+        ) + avail_levels
+    title = _(u'Study Levels Overview')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler checks if a given level has been registered. It returns
+        the ``level_session`` attribute of the student study level object
+        if the named level exists.
+        """
+        if name in self.avail_levels and context is not None:
+            value = ''
+            for level in context['studycourse'].values():
+                if level.level == int(name):
+                    value = '%s' % level.level_session
+                    break
+        return super(
+            StudentExporter, self).mangle_value(
+            value, name, context=context)
+
+class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
+    """Like all other exporters the Combo Card Data Exporter first filters the
+    set of students by searching the students catalog. Then it exports some
+    student base data which are neccessary to print for the Interswitch combo 
+    card (identity card for students). The output contains a ``passport_path``
+    column which contains the filesystem path of the passport image file.
+    If no path is given, no passport image file exists.
+    """
+    grok.name('combocard')
+
+    fields = ('display_fullname',
+              'student_id','matric_number',
+              'certificate', 'faculty', 'department', 'passport_path')
+    title = _(u'Combo Card Data')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the titles of faculty, department
+        and certificate. It also computes the path of passport image file
+        stored in the filesystem.
+        """
+        certificate = context['studycourse'].certificate
+        if name == 'certificate' and certificate is not None:
+            value = certificate.title
+        if name == 'department' and certificate is not None:
+            value = certificate.__parent__.__parent__.longtitle
+        if name == 'faculty' and certificate is not None:
+            value = certificate.__parent__.__parent__.__parent__.longtitle
+        if name == 'passport_path' and certificate is not None:
+            file_id = IFileStoreNameChooser(context).chooseName(
+                attr='passport.jpg')
+            os_path = getUtility(IExtFileStore)._pathFromFileID(file_id)
+            if not os.path.exists(os_path):
+                value = None
+            else:
+                value = '/'.join(os_path.split('/')[-4:])
+        return super(
+            ComboCardDataExporter, self).mangle_value(
+            value, name, context=context)
+
+class TranscriptDataExporter(StudentExporter):
+    """The Transcript Data Exporter first filters the set of
+    students by searching the students catalog. Then it exports student data
+    along with their transcript data.
+    """
+    grok.name('transcriptdata')
+
+    fields = ('student_id', ) + (
+        'state', 'certcode', 'faccode', 'depcode',
+        'entry_session', 'current_level', 'current_session',
+        'transcript_data')
+    title = _(u'Transcript Data')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines and formats the transcript data.
+        """
+        if name == 'transcript_data':
+            value = {}
+            td = context['studycourse'].getTranscriptData()[0]
+            for level in td:
+                tickets_1 = ','.join(i.code for i in level['tickets_1'])
+                tickets_2 = ','.join(i.code for i in level['tickets_2'])
+                tickets_3 = ','.join(i.code for i in level['tickets_3'])
+                value = "Level %s; 1st: %s; 2nd: %s; 3rd: %s; sgpa: %s" % (
+                    level['level_key'], tickets_1, tickets_2,
+                    tickets_3, level['sgpa'],
+                    )
+        return super(
+            TranscriptDataExporter, self).mangle_value(
+            value, name, context=context)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/fileviewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/fileviewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/fileviewlets.py	(revision 16045)
@@ -0,0 +1,173 @@
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+
+import os
+import grok
+from zope.component import getUtility
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser, IKofaObject, IKofaUtils)
+from waeup.kofa.utils.helpers import string_from_bytes, file_size
+
+from waeup.kofa.students.interfaces import IStudent, IStudentsUtils
+
+from waeup.kofa.browser.fileviewlets import (
+    FileDisplay, FileUpload, Image)
+
+from waeup.kofa.browser.layout import (
+    default_filedisplay_template,
+    default_fileupload_template)
+
+from waeup.kofa.students.browser import (
+    StudentBaseDisplayFormPage, StudentBaseManageFormPage,
+    StudentClearanceDisplayFormPage, StudentClearanceManageFormPage,
+    ExportPDFClearanceSlip, StudentFilesUploadPage)
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+# File viewlet baseclasses for student base page
+
+class StudentFileDisplay(FileDisplay):
+    """Base file display viewlet.
+    """
+    grok.baseclass()
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.order(1)
+    grok.require('waeup.viewStudent')
+
+
+class StudentFileUpload(FileUpload):
+    """Base upload viewlet.
+    """
+    grok.baseclass()
+    grok.context(IStudent)
+    grok.view(StudentClearanceManageFormPage)
+    grok.require('waeup.uploadStudentFile')
+
+    @property
+    def mus(self):
+        students_utils = getUtility(IStudentsUtils)
+        return 1024 * students_utils.MAX_KB
+
+    @property
+    def show_viewlet(self):
+        students_utils = getUtility(IStudentsUtils)
+        if self.__name__ in students_utils.SKIP_UPLOAD_VIEWLETS:
+            return False
+        return True
+
+
+class StudentImage(Image):
+    """Renders images for students.
+    """
+    grok.baseclass()
+    grok.context(IStudent)
+    grok.require('waeup.viewStudent')
+
+
+# File viewlets for student base page
+
+class PassportDisplay(StudentFileDisplay):
+    """Passport display viewlet.
+    """
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    grok.template('imagedisplay')
+    label = _(u'Passport Picture')
+    download_name = u'passport.jpg'
+
+
+class PassportUploadManage(StudentFileUpload):
+    """Passport upload viewlet for officers.
+    """
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseManageFormPage)
+    grok.require('waeup.manageStudent')
+    grok.template('imageupload')
+    label = _(u'Passport Picture (jpg only)')
+    download_name = u'passport.jpg'
+    tab_redirect = '#tab2'
+
+    @property
+    def mus(self):
+        kofa_utils = getUtility(IKofaUtils)
+        return kofa_utils.MAX_PASSPORT_SIZE
+
+
+class PassportUploadEdit(PassportUploadManage):
+    """Passport upload viewlet for students.
+    """
+    grok.view(StudentFilesUploadPage)
+    grok.require('waeup.uploadStudentFile')
+
+
+class BirthCertificateDisplay(StudentFileDisplay):
+    """Birth Certificate display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Birth Certificate')
+    title = _(u'Birth Certificate Scan')
+    download_name = u'birth_certificate'
+
+
+class BirthCertificateSlip(BirthCertificateDisplay):
+    grok.view(ExportPDFClearanceSlip)
+
+
+class BirthCertificateUpload(StudentFileUpload):
+    """Birth Certificate upload viewlet.
+    """
+    grok.order(1)
+    label = _(u'Birth Certificate')
+    title = _(u'Birth Certificate Scan')
+    download_name = u'birth_certificate'
+    tab_redirect = '#tab2-top'
+
+
+class Passport(StudentImage):
+    """Renders jpeg passport picture.
+    """
+    grok.name('passport.jpg')
+    download_name = u'passport.jpg'
+    grok.context(IStudent)
+
+
+class ApplicationSlipImage(StudentImage):
+    """Renders application slip scan.
+    """
+    grok.name('application_slip')
+    download_name = u'application_slip'
+
+
+class FinalTranscriptImage(StudentImage):
+    """Renders final transcript.
+    """
+    grok.name('final_transcript')
+    download_name = u'final_transcript'
+
+
+class BirthCertificateImage(StudentImage):
+    """Renders birth certificate scan.
+    """
+    grok.name('birth_certificate')
+    download_name = u'birth_certificate'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/interfaces.py	(revision 16045)
@@ -0,0 +1,956 @@
+## $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
+##
+#from datetime import datetime
+from zope.component import getUtility
+from zope.interface import Attribute, Interface, invariant, Invalid
+from zope import schema
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.kofa.browser.interfaces import IStudentNavigationBase
+from waeup.kofa.interfaces import (
+    IKofaObject, academic_sessions_vocab, validate_email, ICSVExporter,
+    ContextualDictSourceFactoryBase, IKofaUtils)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.schema import TextLineChoice, FormattedDate, PhoneNumber
+from waeup.kofa.students.vocabularies import (
+    StudyLevelSource, contextual_reg_num_source, contextual_mat_num_source,
+    GenderSource, nats_vocab
+    )
+from waeup.kofa.payments.interfaces import (
+    IPaymentsContainer, IOnlinePayment)
+from waeup.kofa.university.vocabularies import (
+    CourseSource, StudyModeSource, CertificateSource,
+    SemesterSource, CourseCategorySource
+    )
+
+class PreviousPaymentCategorySource(ContextualDictSourceFactoryBase):
+    """A source that delivers all selectable categories of previous session
+    payments.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'PREVIOUS_PAYMENT_CATEGORIES'
+
+class BalancePaymentCategorySource(ContextualDictSourceFactoryBase):
+    """A source that delivers all selectable items of balance payments.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'BALANCE_PAYMENT_CATEGORIES'
+
+# VerdictSource can't be placed into the vocabularies module because it
+# requires importing IStudentsUtils which then leads to circular imports.
+class VerdictSource(BasicContextualSourceFactory):
+    """A verdicts source delivers all verdicts provided
+    in the portal.
+    """
+    def getValues(self, context):
+        verdicts_dict = getUtility(IKofaUtils).VERDICTS_DICT
+        return sorted(verdicts_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        verdicts_dict = getUtility(IKofaUtils).VERDICTS_DICT
+        if value != '0':
+            return verdicts_dict[value] + ' (%s)' % value
+        return verdicts_dict[value]
+
+
+class IStudentsUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+    def setReturningData(student):
+        """ This method defines what happens after school fee payment
+        depending on the student's senate verdict.
+
+        In the base configuration current level is always increased
+        by 100 no matter which verdict has been assigned.
+        """
+
+    def setPaymentDetails(category, student, previous_session=None,
+            previous_level=None, combi=None):
+        """Create Payment object and set the payment data of a student for
+        the payment category specified.
+        """
+
+    def increaseMatricInteger(student):
+        """Increase counter for matric numbers.
+
+        This counter can be a centrally stored attribute or an attribute of
+        faculties, departments or certificates. In the base package the counter
+        is as an attribute of the site configuration object.
+        """
+
+    def constructMatricNumber(student):
+        """Fetch the matric number counter which fits the student and
+        construct the new matric number of the student.
+
+        In the base package the counter is returned which is as an attribute
+        of the site configuration object.
+        """
+
+    def setMatricNumber(student):
+        """Set matriculation number of student.
+
+        If the student's matric number is unset a new matric number is
+        constructed according to the matriculation number construction rules
+        defined in the constructMatricNumber method. The new matric number is
+        set, the students catalog updated. The corresponding matric number
+        counter is increased by one.
+
+        This method is tested but not used in the base package. It can
+        be used in custom packages by adding respective views
+        and by customizing increaseMatricInteger and constructMatricNumber
+        according to the university's matriculation number construction rules.
+
+        The method can be disabled by setting the counter to zero.
+        """
+
+    def getAccommodation_details(student):
+        """Determine the accommodation dates of a student.
+        """
+
+    def selectBed(available_beds):
+        """Select a bed from a list of available beds.
+        In the standard configuration we select the first bed found,
+        but can also randomize the selection if we like.
+        """
+
+    def getDegreeClassNumber(level_obj):
+        """Get degree class number (used for SessionResultsPresentation
+        reports).
+        """
+
+    def getPDFCreator(context):
+        """Get some IPDFCreator instance suitable for use with `context`.
+        """
+
+    def renderPDF(view, subject='', filename='slip.pdf',):
+        """Render pdf slips for various pages.
+        """
+
+    def renderPDFAdmissionLetter(view, student=None, omit_fields=(),
+                                 pre_text=None, post_text=None,
+                                 topMargin = None,
+                                 letterhead_path=None):
+        """Render pdf admission letter.
+        """
+
+    def renderPDFTranscript(view, filename, student,
+                  studentview, note, signatures, sigs_in_footer,
+                  show_scans, topMargin, omit_fields,
+                  tableheader, no_passport):
+        """Render pdf slip of a transcripts.
+        """
+
+    def renderPDFCourseticketsOverview(
+                  view, name, session, data, lecturers, orientation,
+                  title_length, note):
+        """Render pdf slip of course tickets for a lecturer.
+        """
+
+    def warnCreditsOOR(studylevel, course=None):
+        """Return message if credits are out of range. In the base
+        package only maximum credits is set.
+        """
+
+    def getBedCoordinates(bedticket):
+        """Return descriptive bed coordinates.
+        This method can be used to customize the `display_coordinates`
+        property method in order to  display a
+        customary description of the bed space.
+        """
+
+    def clearance_disabled_message(student):
+        """Render message if clearance is disabled.
+        """
+
+    def updateCourseTickets(course):
+        """Udate registered course tickets if course
+        attributes were changed.
+        """
+
+class IStudentsContainer(IKofaObject):
+    """A students container contains university students.
+    """
+    def addStudent(student):
+        """Add an IStudent object and subcontainers.
+        """
+
+    unique_student_id = Attribute('A unique student id')
+
+class IStudentNavigation(IStudentNavigationBase):
+    """Interface needed for navigation and logging. This interface is
+    implemented by all content classes in the students section.
+    """
+    student = Attribute('Student object of context')
+
+    def writeLogMessage(view, message):
+        """Add an INFO message to students.log.
+        """
+
+class IStudentBase(IKofaObject):
+    """Representation of student base data.
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Registration state')
+    translated_state = Attribute('Real name of the registration state')
+    certcode = Attribute('Certificate code of any chosen study course')
+    depcode = Attribute('Department code of any chosen study course')
+    faccode = Attribute('Faculty code of any chosen study course')
+    entry_session = Attribute('Entry session')
+    current_session = Attribute('Current session')
+    current_level = Attribute('Current level')
+    current_mode = Attribute('Current mode')
+    current_verdict = Attribute('Current verdict')
+    fullname = Attribute('All name parts separated by hyphens')
+    display_fullname = Attribute('Fullname as displayed on pages')
+    is_postgrad = Attribute('True if postgraduate student')
+    is_special_postgrad = Attribute('True if special postgraduate student')
+    is_fresh = Attribute('True if fresh student')
+    before_payment = Attribute('True if no previous payment has to be made')
+    personal_data_expired = Attribute('True if personal data expired')
+    transcript_enabled = Attribute('True if transcript processing is enabled')
+    clearance_locked = Attribute('True if clearance form is locked')
+    studycourse_locked = Attribute(
+        'True if nobody is allowed to change studycourse, studylecel or '
+        'course ticket data, neither through the UI nor via import')
+
+    password = Attribute('Encrypted password')
+    temp_password = Attribute('Dictionary with user name, timestamp and encrypted password')
+    parents_password = Attribute('Dictionary with student_id, timestamp and encrypted password')
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        default = False,
+        required = False,
+        )
+
+    suspended_comment = schema.Text(
+        title = _(u"Reasons for Deactivation"),
+        required = False,
+        description = _(
+            u'This message will be shown if and only if deactivated '
+            'students try to login.'),
+        )
+
+    flash_notice = schema.TextLine(
+        title = _(u'Flash Notice'),
+        required = False,
+        readonly = False,
+        description = _(
+            u'This single-line message will be shown in a flash box.'),
+        )
+
+    student_id = schema.TextLine(
+        title = _(u'Student Id'),
+        required = False,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    middlename = schema.TextLine(
+        title = _(u'Middle Name'),
+        required = False,
+        )
+
+    lastname = schema.TextLine(
+        title = _(u'Last Name (Surname)'),
+        required = True,
+        )
+
+    sex = schema.Choice(
+        title = _(u'Gender'),
+        source = GenderSource(),
+        required = True,
+        )
+
+    reg_number = TextLineChoice(
+        title = _(u'Registration Number'),
+        required = True,
+        readonly = False,
+        source = contextual_reg_num_source,
+        )
+
+    matric_number = TextLineChoice(
+        title = _(u'Matriculation Number'),
+        required = False,
+        readonly = False,
+        source = contextual_mat_num_source,
+        )
+
+    adm_code = schema.TextLine(
+        title = _(u'PWD Activation Code'),
+        required = False,
+        readonly = False,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email'),
+        required = False,
+        constraint=validate_email,
+        )
+
+    phone = PhoneNumber(
+        title = _(u'Phone'),
+        required = False,
+        )
+
+    parents_email = schema.ASCIILine(
+        title = _(u"Parents' Email"),
+        required = False,
+        constraint=validate_email,
+        )
+
+    def setTempPassword(user, password):
+        """Set a temporary password (LDAP-compatible) SSHA encoded for
+        officers.
+        """
+
+    def getTempPassword():
+        """Check if a temporary password has been set and if it
+        is not expired. Return the temporary password if valid,
+        None otherwise. Unset the temporary password if expired.
+        """
+
+    def setParentsPassword(password):
+        """Set a parents password (LDAP-compatible) SSHA encoded for
+        parents.
+        """
+
+    def getParentsPassword():
+        """Check if a parents password has been set and if it
+        is not expired.
+
+        Return the parents password if valid,
+        None otherwise. Unset the parents password if expired.
+        """
+
+    def transfer(certificate, current_session,
+        current_level, current_verdict):
+        """ Creates a new studycourse and backups the old one.
+        """
+
+    def revert_transfer():
+        """ Revert previous transfer.
+        """
+
+class IUGStudentClearance(IKofaObject):
+    """Representation of undergraduate student clearance data.
+    """
+    officer_comment = schema.Text(
+        title = _(u"Officer's Comment"),
+        required = False,
+        )
+
+    clr_code = schema.TextLine(
+        title = _(u'CLR Activation Code'),
+        required = False,
+        readonly = False,
+        )
+
+    date_of_birth = FormattedDate(
+        title = _(u'Date of Birth'),
+        required = True,
+        show_year = True,
+        )
+
+    nationality = schema.Choice(
+        vocabulary = nats_vocab,
+        title = _(u'Nationality'),
+        required = False,
+        )
+
+class IPGStudentClearance(IUGStudentClearance):
+    """Representation of postgraduate student clearance data.
+    """
+    employer = schema.TextLine(
+        title = _(u'Employer'),
+        required = False,
+        readonly = False,
+        )
+
+class IStudentPersonal(IKofaObject):
+    """Representation of student personal data.
+    """
+    personal_updated = schema.Datetime(
+        title = _(u'Updated'),
+        required = False,
+        readonly = False,
+        )
+
+    perm_address = schema.Text(
+        title = _(u'Permanent Address'),
+        required = False,
+        )
+
+class IStudent(IStudentBase,IUGStudentClearance,IPGStudentClearance,
+    IStudentPersonal):
+    """Representation of a student.
+    """
+
+class IStudentPersonalEdit(IStudentPersonal):
+    """Interface for editing personal data by students.
+    Here we can repeat the fields from IStudentPersonal and set the
+    `required` if necessary.
+    """
+
+    perm_address = schema.Text(
+        title = _(u'Permanent Address'),
+        required = True,
+        )
+
+class IStudentUpdateByRegNo(IStudent):
+    """Representation of a student. Skip regular reg_number validation.
+    """
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        required = False,
+        )
+
+class IStudentUpdateByMatricNo(IStudent):
+    """Representation of a student. Skip regular matric_number validation.
+    """
+    matric_number = schema.TextLine(
+        title = _(u'Matriculation Number'),
+        required = False,
+        )
+
+class IStudentRequestPW(IStudent):
+    """Representation of a student for first-time password request.
+    This interface is used when students use the requestpw page to
+    login for the the first time.
+    """
+    number = schema.TextLine(
+        title = _(u'Registr. or Matric. Number'),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+class IStudentStudyCourse(IKofaObject):
+    """Representation of student study course data.
+    """
+    next_session_allowed = Attribute('True if the student can proceed to next session')
+    is_postgrad = Attribute('True if student is postgraduate student')
+    is_current = Attribute('True if the study course is the current course of studies')
+    is_previous = Attribute('True if the study course is the previous course of studies')
+
+    certificate = schema.Choice(
+        title = _(u'Certificate'),
+        source = CertificateSource(),
+        required = False,
+        )
+
+    entry_mode = schema.Choice(
+        title = _(u'Entry Mode'),
+        source = StudyModeSource(),
+        required = True,
+        readonly = False,
+        )
+
+    entry_session = schema.Choice(
+        title = _(u'Entry Session'),
+        source = academic_sessions_vocab,
+        #default = datetime.now().year,
+        required = True,
+        readonly = False,
+        )
+
+    current_session = schema.Choice(
+        title = _(u'Current Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        readonly = False,
+        )
+
+    current_level = schema.Choice(
+        title = _(u'Current Level'),
+        source = StudyLevelSource(),
+        required = False,
+        readonly = False,
+        )
+
+    current_verdict = schema.Choice(
+        title = _(u'Current Verdict'),
+        source = VerdictSource(),
+        default = '0',
+        required = False,
+        )
+
+    previous_verdict = schema.Choice(
+        title = _(u'Previous Verdict'),
+        source = VerdictSource(),
+        default = '0',
+        required = False,
+        )
+
+    def addStudentStudyLevel(cert, studylevel):
+        """Add a study level object.
+        """
+
+    def getTranscriptData():
+        """Get a sorted list of dicts with level and course ticket data.
+        This method is used for transcripts.
+        """
+
+class IStudentStudyCourseTransfer(IStudentStudyCourse):
+    """An interface used for student transfers.
+    """
+    certificate = schema.Choice(
+        title = _(u'Certificate'),
+        source = CertificateSource(),
+        required = True,
+        )
+
+    current_level = schema.Choice(
+        title = _(u'Current Level'),
+        source = StudyLevelSource(),
+        required = True,
+        readonly = False,
+        )
+
+    entry_session = schema.Choice(
+        title = _(u'Entry Session'),
+        source = academic_sessions_vocab,
+        #default = datetime.now().year,
+        required = False,
+        readonly = False,
+        )
+
+
+IStudentStudyCourseTransfer['certificate'].order = IStudentStudyCourse[
+    'certificate'].order
+IStudentStudyCourseTransfer['current_level'].order = IStudentStudyCourse[
+    'current_level'].order
+
+class IStudentVerdictUpdate(IKofaObject):
+    """A interface for verdict imports.
+    """
+    current_verdict = schema.Choice(
+        title = _(u'Current Verdict'),
+        source = VerdictSource(),
+        required = True,
+        )
+
+    current_session = schema.Choice(
+        title = _(u'Current Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    current_level = schema.Choice(
+        title = _(u'Current Level'),
+        source = StudyLevelSource(),
+        required = True,
+        )
+
+    bypass_validation = schema.Bool(
+        title = _(u'Bypass validation'),
+        required = False,
+        )
+
+    validated_by = schema.TextLine(
+        title = _(u'Validated by'),
+        required = False,
+        )
+
+class IStudentStudyLevel(IKofaObject):
+    """A representation of student study level data. 
+    """
+    certcode = Attribute('The certificate code of the study course')
+    is_current_level = Attribute('True if level is current level of the student')
+    level_title = Attribute('Level title from source')
+    getSessionString = Attribute('Session title from source')
+    number_of_tickets = Attribute('Number of tickets contained in this level')
+    passed_params = Attribute('Information about passed and failed courses')
+    gpa_params_rectified = Attribute('Corrected sessional GPA parameters')
+    gpa_params = Attribute('GPA parameters for this level.')
+    cumulative_params = Attribute(
+        'Cumulative GPA and other cumulative parameters for this level')
+    course_registration_forbidden = Attribute(
+        'Return error message if course registration is forbidden')
+
+    level = schema.Choice(
+        title = _(u'Level'),
+        source = StudyLevelSource(),
+        required = True,
+        readonly = False,
+        )
+
+    level_session = schema.Choice(
+        title = _(u'Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    level_verdict = schema.Choice(
+        title = _(u'Verdict'),
+        source = VerdictSource(),
+        default = '0',
+        required = False,
+        )
+
+    validated_by = schema.TextLine(
+        title = _(u'Validated by'),
+        default = None,
+        required = False,
+        )
+
+    validation_date = schema.Datetime(
+        title = _(u'Validation Date'),
+        required = False,
+        readonly = False,
+        )
+
+    total_credits = schema.Int(
+        title = _(u'Total Credits'),
+        required = False,
+        readonly = True,
+        )
+
+    gpa = schema.TextLine(
+        title = _(u'Unrectified GPA'),
+        required = False,
+        readonly = True,
+        )
+
+    transcript_remark = schema.TextLine(
+        title = _(u'Transcript Remark'),
+        required = False,
+        readonly = False,
+        )
+
+    def addCourseTicket(ticket, course):
+        """Add a course ticket object.
+        """
+
+    def addCertCourseTickets(cert):
+        """Collect all certificate courses and create course
+        tickets automatically.
+        """
+
+    def updateCourseTicket(ticket, course):
+        """Updates a course ticket object and return code
+        if ticket has been invalidated.
+        """
+
+class ICourseTicket(IKofaObject):
+    """A representation of course ticket data.
+    """
+    certcode = Attribute('Certificate code of the study course')
+    level_session = Attribute('Session of the study level the ticket has been added to')
+    level = Attribute('Level value of the study level the ticket has been added to')
+    total_score = Attribute('Score')
+    grade = Attribute('Grade calculated from total score')
+    weight = Attribute('Weight calculated from total score')
+    removable_by_student = Attribute('True if student is allowed to remove the ticket')
+    editable_by_lecturer = Attribute('True if lecturer is allowed to edit the ticket')
+
+    code = Attribute('Code of the original course')
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        required = False,
+        )
+
+    fcode = schema.TextLine(
+        title = _(u'Faculty Code'),
+        required = False,
+        )
+
+    dcode = schema.TextLine(
+        title = _(u'Department Code'),
+        required = False,
+        )
+
+    semester = schema.Choice(
+        title = _(u'Semester/Term'),
+        source = SemesterSource(),
+        required = False,
+        )
+
+    ticket_session = schema.Choice(
+        title = _(u'Imported Session'),
+        source = academic_sessions_vocab,
+        required = False,
+        )
+
+    passmark = schema.Int(
+        title = _(u'Passmark'),
+        required = False,
+        )
+
+    credits = schema.Int(
+        title = _(u'Credits'),
+        required = False,
+        )
+
+    mandatory = schema.Bool(
+        title = _(u'Required'),
+        default = False,
+        required = False,
+        )
+
+    outstanding = schema.Bool(
+        title = _(u'Outstanding Course'),
+        default = False,
+        required = False,
+        )
+
+    course_category = schema.Choice(
+        title = _(u'Course Category'),
+        source = CourseCategorySource(),
+        required = False,
+        )
+
+    score = schema.Int(
+        title = _(u'Score'),
+        default = None,
+        required = False,
+        missing_value = None,
+        )
+
+    carry_over = schema.Bool(
+        title = _(u'Carry-over Course'),
+        default = False,
+        required = False,
+        )
+
+    automatic = schema.Bool(
+        title = _(u'Automatical Creation'),
+        default = False,
+        required = False,
+        )
+
+class ICourseTicketAdd(IKofaObject):
+    """An interface for adding course tickets.
+    """
+    course = schema.Choice(
+        title = _(u'Course'),
+        source = CourseSource(),
+        readonly = False,
+        )
+
+class ICourseTicketImport(ICourseTicket):
+    """An interface for importing course results and nothing more.
+    """
+    score = schema.Int(
+        title = _(u'Score'),
+        required = False,
+        readonly = False,
+        )
+
+    level_session = schema.Choice(
+        title = _(u'Level Session'),
+        source = academic_sessions_vocab,
+        required = False,
+        readonly = False,
+        )
+
+class IStudentAccommodation(IKofaObject):
+    """A container for student accommodation objects.
+    """
+
+    desired_hostel = schema.TextLine(
+        title = _(u'Desired Hostel'),
+        required = False,
+        )
+
+    def addBedTicket(bedticket):
+        """Add a bed ticket object.
+        """
+
+
+class IBedTicket(IKofaObject):
+    """A representation of accommodation booking data.
+    """
+    bed = Attribute('The bed object')
+    maint_payment_made = Attribute('True if maintenance payment is made')
+
+    display_coordinates = schema.TextLine(
+        title = _(u'Allocated Bed'),
+        required = False,
+        readonly = True,
+        )
+
+    bed_coordinates = schema.TextLine(
+        title = u'',
+        required = True,
+        readonly = False,
+        )
+
+    bed_type = schema.TextLine(
+        title = _(u'Requested Bed Type'),
+        required = True,
+        readonly = False,
+        )
+
+    booking_session = schema.Choice(
+        title = _(u'Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        readonly = False
+        )
+
+    booking_date = schema.Datetime(
+        title = _(u'Booking Date'),
+        required = False,
+        readonly = False,
+        )
+
+    booking_code = schema.TextLine(
+        title = _(u'Booking Activation Code'),
+        required = False,
+        readonly = False,
+        )
+
+    def getSessionString():
+        """Returns the title of academic_sessions_vocab term of the session
+        when the bed was booked.
+        """
+
+class IStudentPaymentsContainer(IPaymentsContainer):
+    """A container for student payment objects.
+    """
+
+    certificate = Attribute('Certificate to determine the correct p_level value')
+
+class IStudentOnlinePayment(IOnlinePayment):
+    """A student payment via payment gateways.
+    """
+
+    certificate = Attribute('Certificate to determine the correct p_level value')
+    student = Attribute('Student')
+
+    p_current = schema.Bool(
+        title = _(u'Current Session Payment'),
+        default = True,
+        required = False,
+        )
+
+    p_level = schema.Choice(
+        title = _(u'Payment Level'),
+        source = StudyLevelSource(),
+        required = False,
+        )
+
+    def redeemTicket():
+        """Either create an appropriate access code or trigger an action
+        directly.
+        """
+
+    def doAfterStudentPayment():
+        """Process student after payment was made.
+        """
+
+    def doAfterStudentPaymentApproval():
+        """Process student after payment was approved.
+        """
+
+    def approveStudentPayment():
+        """Approve payment and process student.
+        """
+
+IStudentOnlinePayment['p_level'].order = IStudentOnlinePayment[
+    'p_session'].order
+
+class IStudentPreviousPayment(IKofaObject):
+    """An interface for adding previous session payments.
+    """
+
+    p_category = schema.Choice(
+        title = _(u'Payment Category'),
+        default = u'schoolfee',
+        source = PreviousPaymentCategorySource(),
+        required = True,
+        )
+
+    p_session = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    p_level = schema.Choice(
+        title = _(u'Payment Level'),
+        source = StudyLevelSource(),
+        required = True,
+        )
+
+class IStudentBalancePayment(IKofaObject):
+    """An interface for adding balances.
+    """
+
+    p_category = schema.Choice(
+        title = _(u'Payment Category'),
+        default = u'schoolfee',
+        required = True,
+        source = BalancePaymentCategorySource(),
+        )
+
+    balance_session = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    balance_level = schema.Choice(
+        title = _(u'Payment Level'),
+        source = StudyLevelSource(),
+        required = True,
+        )
+
+    balance_amount = schema.Float(
+        title = _(u'Balance Amount'),
+        default = None,
+        required = True,
+        readonly = False,
+        description = _(
+            u'Balance in Naira '),
+        )
+
+class ICSVStudentExporter(ICSVExporter):
+    """A regular ICSVExporter that additionally supports exporting
+      data from a given student object.
+    """
+    def get_filtered(site, **kw):
+        """Get a filtered set of students.
+        """
+
+    def get_selected(site, selected):
+        """Get set of selected students.
+        """
+
+    def export_student(student, filepath=None):
+        """Export data for a given student.
+        """
+
+    def export_filtered(site, filepath=None, **kw):
+        """Export data for filtered set of students.
+        """
+
+    def export_selected(site, filepath=None, **kw):
+        """Export data for selected set of students.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/payments.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/payments.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/payments.py	(revision 16045)
@@ -0,0 +1,246 @@
+## $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
+##
+"""
+Student payment components.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.schema.interfaces import ConstraintNotSatisfied
+from hurry.workflow.interfaces import IWorkflowInfo
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import (
+    IStudentPaymentsContainer, IStudentNavigation, IStudentOnlinePayment)
+from waeup.kofa.students.workflow import CLEARED, RETURNING, PAID
+from waeup.kofa.payments import PaymentsContainer, OnlinePayment
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.accesscodes import create_accesscode
+
+class StudentPaymentsContainer(PaymentsContainer):
+    """This is a container for student payments.
+    """
+    grok.implements(IStudentPaymentsContainer, IStudentNavigation)
+    grok.provides(IStudentPaymentsContainer)
+
+    def __init__(self):
+        super(StudentPaymentsContainer, self).__init__()
+        return
+
+    @property
+    def student(self):
+        return self.__parent__
+
+    @property
+    def certificate(self):
+        try:
+            return self.student['studycourse'].certificate
+        except TypeError:
+            return None
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.writeLogMessage(view, message)
+
+StudentPaymentsContainer = attrs_to_fields(StudentPaymentsContainer)
+
+class StudentOnlinePayment(OnlinePayment):
+    """This is an online payment.
+    """
+    grok.implements(IStudentOnlinePayment, IStudentNavigation)
+    grok.provides(IStudentOnlinePayment)
+
+    def __init__(self):
+        super(StudentOnlinePayment, self).__init__()
+        return
+
+    @property
+    def student(self):
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
+
+    @property
+    def certificate(self):
+        try:
+            return self.student['studycourse'].certificate
+        except TypeError:
+            return None
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    def redeemTicket(self):
+        """Either create an appropriate access code or trigger an action
+        directly.
+        """
+        student = self.student
+        if self.p_category == 'clearance':
+            # Create CLR access code
+            pin, error = create_accesscode(
+                'CLR',0,self.amount_auth,student.student_id)
+            if error:
+                return error
+            self.ac = pin
+        elif self.p_category.startswith('schoolfee'):
+            # Bypass activation code creation if next session
+            # can be started directly.
+            if student['studycourse'].next_session_allowed:
+                try:
+                    if student.state == CLEARED:
+                        IWorkflowInfo(student).fireTransition(
+                            'pay_first_school_fee')
+                        return None
+                    elif student.state == RETURNING:
+                        IWorkflowInfo(student).fireTransition(
+                            'pay_school_fee')
+                        return None
+                    elif student.state == PAID:
+                        IWorkflowInfo(student).fireTransition(
+                            'pay_pg_fee')
+                        return None
+                except ConstraintNotSatisfied:
+                    pass
+            # Create SFE access code
+            pin, error = create_accesscode(
+                'SFE',0,self.amount_auth,student.student_id)
+            if error:
+                return error
+            self.ac = pin
+        elif self.p_category == 'bed_allocation':
+            # Create HOS access code
+            pin, error = create_accesscode(
+                'HOS',0,self.amount_auth,student.student_id)
+            if error:
+                return error
+            self.ac = pin
+        elif self.p_category == 'transcript':
+            # Create TSC access code
+            pin, error = create_accesscode(
+                'TSC',0,self.amount_auth,student.student_id)
+            if error:
+                return error
+            self.ac = pin
+        return None
+
+    def doAfterStudentPayment(self):
+        """Process student after payment was made.
+        """
+        if self.p_current:
+            error = self.redeemTicket()
+            if error is not None:
+                return 'danger', error, error
+        log = 'successful %s payment: %s' % (self.p_category, self.p_id)
+        msg = _('Payment successfully completed')
+        flashtype = 'success'
+        return flashtype, msg, log
+
+    def doAfterStudentPaymentApproval(self):
+        """Process student after payment was approved.
+        """
+        if self.p_current:
+            error = self.redeemTicket()
+            if error is not None:
+                return 'danger', error, error
+        log = '%s payment approved: %s' % (self.p_category, self.p_id)
+        msg = _('Payment approved')
+        flashtype = 'success'
+        return flashtype, msg, log
+
+    def approveStudentPayment(self):
+        """Approve payment and process student.
+        """
+        if self.p_state == 'paid':
+            return 'warning', _('This ticket has already been paid.'), None
+        self.approve()
+        return self.doAfterStudentPaymentApproval()
+
+
+StudentOnlinePayment = attrs_to_fields(
+    StudentOnlinePayment, omit=['display_item'])
+
+class Payer(grok.Adapter):
+    """An adapter to publish student data through a simple webservice.
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.implements(IPayer)
+
+    @property
+    def display_fullname(self):
+        "Name of  payer"
+        return self.context.student.display_fullname
+
+    @property
+    def id(self):
+        "Id of payer"
+        return self.context.student.student_id
+
+    @property
+    def matric_number(self):
+        "Matric number or reg number of payer"
+        return self.context.student.matric_number
+
+    @property
+    def reg_number(self):
+        "Reg number or reg number of payer"
+        return self.context.student.reg_number
+
+    @property
+    def faculty(self):
+        "Faculty of payer"
+        return self.context.student.faccode
+
+    @property
+    def department(self):
+        "Department of payer"
+        return self.context.student.depcode
+
+    @property
+    def email(self):
+        "Email of payer"
+        return self.context.student.email
+
+    @property
+    def phone(self):
+        "Phone number of payer"
+        return self.context.student.phone
+
+    @property
+    def current_mode(self):
+        "Current study mode of payer"
+        return self.context.student.current_mode
+
+    @property
+    def current_level(self):
+        "Current level of payer"
+        return self.context.student.current_level
+
+# Student online payments must be importable. So we might need a factory.
+class StudentOnlinePaymentFactory(grok.GlobalUtility):
+    """A factory for student online payments.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.StudentOnlinePayment')
+    title = u"Create a new online payment.",
+    description = u"This factory instantiates new online payment instances."
+
+    def __call__(self, *args, **kw):
+        return StudentOnlinePayment()
+
+    def getInterfaces(self):
+        return implementedBy(StudentOnlinePayment)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/permissions.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/permissions.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/permissions.py	(revision 16045)
@@ -0,0 +1,223 @@
+## $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
+##
+"""
+Permissions for the students section.
+"""
+import grok
+
+# Students section permissions
+
+class HandleStudent(grok.Permission):
+    """
+    The HandleStudent permission is reserved for students.
+    Students 'handle' their data. Officers 'manage' the data.
+    """
+    grok.name('waeup.handleStudent')
+
+class ViewStudent(grok.Permission):
+    """
+    The ViewStudent permission allows to view all student data.
+    """
+    grok.name('waeup.viewStudent')
+
+class ViewMyStudentDataTab(grok.Permission):
+    grok.name('waeup.viewMyStudentDataTab')
+
+class ViewStudentsContainer(grok.Permission):
+    """The ViewStudentsContainer permission allows to view the students root
+    container page.
+    """
+    grok.name('waeup.viewStudentsContainer')
+
+class PayStudent(grok.Permission):
+    """The PayStudent permission allows to add an online payment ticket and to
+    manage tickets.
+    """
+    grok.name('waeup.payStudent')
+
+class HandleAccommodation(grok.Permission):
+    """The HandleAccommodation allows to manage bed tickets.
+    """
+    grok.name('waeup.handleAccommodation')
+
+class UploadStudentFile(grok.Permission):
+    """The UploadStudentFile permissions allows to upload the passport picture.
+    The respective page additionally checks the state of the student.
+    """
+    grok.name('waeup.uploadStudentFile')
+
+class ManageStudent(grok.Permission):
+    """The ManageStudent permission allows to edit the data.
+    This permission is meant for students officers.
+    """
+    grok.name('waeup.manageStudent')
+
+class ClearStudent(grok.Permission):
+    """The ClearStudent permission is needed to clear students
+    or to reject clearance. This permission is meant for clearance officers.
+    """
+    grok.name('waeup.clearStudent')
+
+class ValidateStudent(grok.Permission):
+    """The ValidateStudent permission is needed to validate or reject
+    course lists. This permission is not needed if users
+    already have the TriggerTransition permission.
+    """
+    grok.name('waeup.validateStudent')
+
+class EditStudyLevel(grok.Permission):
+    """The EditStudyLevel permission is needed for editing course lists.
+    Students and course advisers do have this permission.
+    """
+    grok.name('waeup.editStudyLevel')
+
+class LoginAsStudent(grok.Permission):
+    """The LoginAsStudent is needed to set temporary student passwords
+    and login as (impersonate) students.
+    """
+    grok.name('waeup.loginAsStudent')
+
+class ViewTranscript(grok.Permission):
+    """The ViewTranscript role is needed to view transcript pages and slips.
+    """
+    grok.name('waeup.viewTranscript')
+
+class ProcessTranscript(grok.Permission):
+    grok.name('waeup.processTranscript')
+    """The ProcessTranscript role is needed to validate and relase transcripts.
+    """
+
+class SignTranscript(grok.Permission):
+    grok.name('waeup.signTranscript')
+    """The SignTranscript role is needed to sign transcripts.
+    """
+
+# Local role
+class StudentRecordOwner(grok.Role):
+    """A student 'owns' her/his student object and subobjects and
+    gains permissions to handle all data, upload a passport picture,
+    add payment tickets, create and edit course lists and handle accommodation.
+    """
+    grok.name('waeup.local.StudentRecordOwner')
+    grok.title(u'Student Record Owner')
+    grok.permissions('waeup.handleStudent',
+                     'waeup.uploadStudentFile',
+                     'waeup.viewStudent',
+                     'waeup.payStudent',
+                     'waeup.handleAccommodation',
+                     'waeup.editStudyLevel')
+
+class Parents(grok.Role):
+    """Parents temporarily get access to view the records of their children.
+    """
+    grok.name('waeup.local.Parents')
+    grok.title(u'Parents')
+    grok.permissions('waeup.viewStudent')
+
+# Site Roles
+class StudentRole(grok.Role):
+    """This role is dedicated to students only.
+    It defines the permissions a student gains portal-wide.
+    """
+    grok.name('waeup.Student')
+    grok.title(u'Student (do not assign)')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.viewMyStudentDataTab',
+                     'waeup.Authenticated')
+
+class StudentsOfficer(grok.Role):
+    """The Students Officer is allowed to view all student data.
+    """
+    grok.name('waeup.StudentsOfficer')
+    grok.title(u'Students Officer (view only)')
+    grok.permissions('waeup.viewStudent',
+                     'waeup.viewStudentsContainer')
+
+class StudentsManager(grok.Role):
+    """The Students Manager is allowed to edit all student data, to
+    create payment tickets, to handle bed tickets and to upload passport
+    pictures.
+    """
+    grok.name('waeup.StudentsManager')
+    grok.title(u'Students Manager')
+    grok.permissions('waeup.viewStudent',
+                     'waeup.manageStudent',
+                     'waeup.viewStudentsContainer',
+                     'waeup.payStudent',
+                     'waeup.uploadStudentFile',
+                     'waeup.handleAccommodation')
+
+class TranscriptOfficer(grok.Role):
+    """The Transcript Officer is allowed to view, to validate and to
+    release student transcripts. The officer is not allowed to
+    manage student data but to edit the transcript remark on a separate
+    manage page.
+    """
+    grok.name('waeup.TranscriptOfficer')
+    grok.title(u'Transcript Officer')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.viewTranscript',
+                     'waeup.processTranscript',
+                     'waeup.viewStudent',
+                     'waeup.viewStudentsContainer',
+                     )
+
+class TranscriptSignee(grok.Role):
+    """The Transcript Signee is allowed to view and to sign student
+    transcripts.
+    """
+    grok.name('waeup.TranscriptSignee')
+    grok.title(u'Transcript Signee')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.viewTranscript',
+                     'waeup.signTranscript',
+                     'waeup.viewStudent',
+                     )
+
+class StudentsClearanceOfficer(grok.Role):
+    """The global StudentsClearanceOfficer role enables users to view all
+    student data, to clear students and to reject clearance portal-wide.
+    Usually, this role is not assigned manually.
+    We are using the correspondent local role instead which assigns the
+    StudentsClearanceOfficer role dynamically.
+    """
+    grok.name('waeup.StudentsClearanceOfficer')
+    grok.title(u'Clearance Officer (all students)')
+    grok.permissions('waeup.clearStudent',
+                     'waeup.viewStudent')
+
+class StudentsCourseAdviser(grok.Role):
+    """The global StudentsCourseAdviser role enables users to view all
+    student data, to edit, validate or reject course lists  portal-wide.
+    Usually, this role is not assigned manually.
+    We are using the correspondent local role instead which assigns the
+    StudentsCourseAdviser role dynamically.
+    """
+    grok.name('waeup.StudentsCourseAdviser')
+    grok.title(u'Course Adviser (all students)')
+    grok.permissions('waeup.validateStudent',
+                     'waeup.viewStudent',
+                     'waeup.editStudyLevel')
+
+class StudentImpersonator(grok.Role):
+    """The Student Impersonator gains the LoginAsStudent permission,
+    nothing else, see description above.
+    """
+    grok.name('waeup.StudentImpersonator')
+    grok.title(u'Student Impersonator')
+    grok.permissions('waeup.loginAsStudent')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt	(revision 16045)
@@ -0,0 +1,53 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    Please pick the certificate, session and level
+    for which the report should be generated.
+    The report will be processed and then made available for you
+    to view/download.
+  </p>
+  <br />
+
+  <label for="certificate" i18n:translate="">Certificate</label>
+  <input id="filterbox" class="kofa-filterbox form-control half"
+         placeholder="Enter search text, then select item below."
+         type="text" i18n:translate=""/>
+  <br />
+  <select id="filteredselect" name="faccode_depcode_certcode"
+          class="form-control" size=5>
+      <span tal:repeat="items view/faccode_depcode_certcode"
+          tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="session">Academic Session</label>
+  <select name="session" class="form-control half">
+    <span tal:repeat="items view/sessions" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="level">Level</label>
+  <select name="level" class="form-control half">
+    <span tal:repeat="items view/levels" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="CREATE" value="Create" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentpaymentstatisticsreportgeneratorpage.pt	(revision 16045)
@@ -0,0 +1,79 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    Please pick the session and study modes group
+    for which the report should be generated.
+    The report data will be calculated and then made available for you
+    to view/download.
+  </p>
+  <br />
+  <label for="breakdown" i18n:translate="">Breakdown</label>
+    <select name="breakdown" class="form-control half">
+      <span tal:repeat="items view/breakdowns" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  <br />
+  <label for="session" i18n:translate="">Study Modes Group</label>
+    <select name="mode" class="form-control half">
+      <span tal:repeat="items view/modes" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  <br />
+  <label for="session">Academic Session</label>
+  <select name="session" class="form-control half">
+    <span tal:repeat="items view/sessions" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="level">Current Level</label>
+  <select name="level" class="form-control half">
+    <span tal:repeat="items view/levels" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="entry_session">Entry Session</label>
+  <select name="entry_session" class="form-control half">
+    <span tal:repeat="items view/sessions_plus" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="p_session">Payment Session</label>
+  <select name="p_session" class="form-control half">
+    <span tal:repeat="items view/sessions_plus" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="CREATE" value="Create" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt	(revision 16045)
@@ -0,0 +1,57 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    Please pick the session and study modes group
+    for which the report should be generated.
+    The report data will be calculated and then made available for you
+    to view/download.
+  </p>
+  <br />
+  <label for="breakdown" i18n:translate="">Breakdown</label>
+    <select name="breakdown" class="form-control half">
+      <span tal:repeat="items view/breakdowns" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  <br />
+  <label for="session" i18n:translate="">Study Modes Group</label>
+    <select name="mode" class="form-control half">
+      <span tal:repeat="items view/modes" tal:omit-tag="">
+        <option
+            tal:define="name python: items[1]; title python: items[0]"
+            tal:attributes="value name">
+          <span tal:replace="title">TITLE</span>
+        </option>
+      </span>
+    </select>
+  <br />
+  <label for="session">Academic Session</label>
+  <select name="session" class="form-control half">
+    <span tal:repeat="items view/sessions" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <label for="level">Current Level</label>
+  <select name="level" class="form-control half">
+    <span tal:repeat="items view/levels" tal:omit-tag="">
+      <option
+          tal:define="name python: items[1]; title python: items[0]"
+          tal:attributes="value name">
+        <span tal:replace="title">TITLE</span>
+      </option>
+    </span>
+  </select>
+  <br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="CREATE" value="Create" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+</form>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/level_report.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/level_report.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/level_report.py	(revision 16045)
@@ -0,0 +1,435 @@
+## $Id$
+##
+## Copyright (C) 2013 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
+##
+import grok
+import textwrap
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils, GRADUATED,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+from waeup.kofa.utils.helpers import get_user_account
+from waeup.kofa.browser.pdf import get_signature_tables
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportPDFView)
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from reportlab.platypus.flowables import Flowable
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+from waeup.kofa.browser.pdf import SMALL_PARA_STYLE
+
+STYLE = getSampleStyleSheet()
+
+class TTR(Flowable): #TableTextRotate
+    '''Rotates a text in a table cell.'''
+
+    def __init__(self, text):
+        Flowable.__init__(self)
+        self.text=text
+
+    def draw(self):
+        canvas = self.canv
+        canvas.rotate(45)
+        canvas.drawString( 0, -1, self.text)
+
+    #def wrap(self,aW,aH):
+    #    canv = self.canv
+    #    return canv._leading, canv.stringWidth(self.text)
+
+def tbl_data_to_table(data):
+    result = []
+    col_names = (
+            'S/N',
+            'Matric No.',
+            translate(_('Student Name')),
+            TTR(translate(_('Total Credits Taken'))),
+            TTR(translate(_('Total Credits Passed'))),
+            TTR(translate(_('GPA'))),
+            translate(_('Courses Failed')),
+            translate(_('Outstanding Courses')),
+            TTR(translate(_('Cum. Credits Taken'))),
+            TTR(translate(_('Cum. Credits Passed'))),
+            TTR(translate(_('CGPA'))),
+            TTR(translate(_('Remark'))))
+    table = [col_names]
+    sn = 1
+    for line in data:
+        # Underline mandatory (core) courses
+        failed_courses = Paragraph(
+            line[5].replace(
+                'm_', '<u>').replace('_m', '</u>'), SMALL_PARA_STYLE)
+        line = line[:5] + (failed_courses,) + line[6:]
+        # Superscript in remarks (needed by AAUE)
+        remark = Paragraph(
+            line[10].replace(
+                's_', '<super>').replace('_s', '</super>'), SMALL_PARA_STYLE)
+        line = line[:10] + (remark,)
+        line = (sn,) + line
+        table.append(line)
+        sn += 1
+    return table
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('VALIGN', (0, 1), (-1,-1), 'TOP'),
+    #('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8), # 1st column
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8), # 1st row
+    #('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8), # last row
+    #('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8), # last column
+    ('ALIGN', (3,1), (-1,-1), 'RIGHT'),
+    ('ALIGN', (0,0), (0,-1), 'RIGHT'),
+    ('ALIGN', (6,0), (6,-1), 'LEFT'),
+    ('ALIGN', (7,0), (7,-1), 'LEFT'),
+    ('INNERGRID', (0,1), (-1,-1), 0.25, colors.black),
+    ('BOX', (0,1), (-1,-1), 1, colors.black),
+    ]
+
+class ILevelReport(IReport):
+
+    session = Attribute('Session to report')
+    level = Attribute('Level to report')
+    faccode = Attribute('Faculty to report')
+    depcode = Attribute('Department to report')
+    certcode = Attribute('Certificate to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+@implementer(ILevelReport)
+class LevelReport(Report):
+    data = None
+    session = None
+    level = None
+    faccode = None
+    depcode = None
+    certcode = None
+    pdfcreator = 'landscape'
+    note = None
+    signatures = None
+
+    @property
+    def title(self):
+        return translate(_('Level Report'))
+
+    @property
+    def right_footer(self):
+        return self.title + ' - %s -' % self.session
+
+    def _excluded(self, level_obj):
+        """Some universities may add further conditions to exclude
+        students from reports. These conditions can be customized in
+        this function.
+        """
+        return False
+
+    def _get_students(self, faccode, depcode, certcode, session, level):
+        """Get students in a certain department, studying a certain programmen
+        who registered courses in a certain session at a certain level.
+
+        Returns a list of student data tuples.
+        """
+        site = grok.getSite()
+        cat = queryUtility(ICatalog, name="students_catalog")
+        if certcode == 'all':
+            certcode = None
+        result = cat.searchResults(
+            depcode = (depcode, depcode), faccode = (faccode, faccode),
+            certcode = (certcode, certcode)
+            )
+        table = []
+        format_float = getUtility(IKofaUtils).format_float
+        for stud in result:
+            if not stud['studycourse'].has_key(str(level)):
+                continue
+            if stud.state == GRADUATED:
+                continue
+            level_obj = stud['studycourse'][str(level)]
+            if level_obj.level_session != session:
+                continue
+            if self._excluded(level_obj):
+                continue
+            passed_params = level_obj.passed_params
+            failed_courses = textwrap.fill(passed_params[4], 35)
+            not_taken_courses = textwrap.fill(passed_params[5], 17)
+            fullname = textwrap.fill(stud.display_fullname, 29)
+            # This is a very special column requested by AAUE, Nigeria.
+            # The 'remark' column remains empty in base package.
+            remark = getattr(level_obj, 'remark', '')
+            end_level = getattr(stud['studycourse'].certificate, 'end_level', None)
+            credits_counted = level_obj.gpa_params[1]
+            credits_passed = passed_params[2]
+            level_gpa = format_float(level_obj.gpa_params[0], 3)
+            cum_credits_taken = level_obj.cumulative_params[1]
+            cum_credits_passed = level_obj.cumulative_params[4]
+            if end_level and level >= end_level:
+                cgpa = format_float(level_obj.cumulative_params[0], 2)
+            else:
+                cgpa = format_float(level_obj.cumulative_params[0], 3)
+            if remark in ('FRNS', 'NER', 'NYV'):
+                cgpa = ''
+                if remark in ('NER', 'NYV'):
+                    # hide all data (requestd by AAUE, Nigeria)
+                    credits_counted = ''
+                    credits_passed = ''
+                    level_gpa = ''
+                    failed_courses = ''
+                    not_taken_courses = ''
+                    cum_credits_taken = ''
+                    cum_credits_passed = ''
+            line = (
+                    stud.matric_number,
+                    fullname,
+                    credits_counted,
+                    credits_passed,
+                    level_gpa,
+                    failed_courses,
+                    not_taken_courses,
+                    cum_credits_taken,
+                    cum_credits_passed,
+                    cgpa,
+                    remark,
+                    )
+            table.append(line)
+        return sorted(table, key=lambda value:value[0])
+
+    def __init__(self, faccode, depcode, certcode, session, level,
+                 author='System'):
+        super(LevelReport, self).__init__(
+            args=[faccode, depcode, certcode, session, level],
+            kwargs={'author':author})
+        site = grok.getSite()
+        self.studylevelsource = StudyLevelSource().factory
+        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.levelcode = level
+        self.sessioncode = session
+        self.faccode = faccode
+        self.depcode = depcode
+        self.certcode = certcode
+        self.factitle = site['faculties'][faccode].longtitle
+        self.deptitle = site['faculties'][faccode][depcode].longtitle
+        if self.certcode == 'all':
+            self.certtitle = translate(
+                _('All Certificates'), 'waeup.kofa',
+                target_language=self.portal_language)
+            self.level = translate(
+                self.studylevelsource.getTitle(None, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        else:
+            certificate = site[
+                'faculties'][faccode][depcode].certificates[certcode]
+            self.certtitle = certificate.longtitle
+            self.level = translate(
+                self.studylevelsource.getTitle(certificate, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = self._get_students(faccode, depcode, certcode, session, level)
+
+    def create_pdf(self, job_id):
+        creator = getUtility(IPDFCreator, name=self.pdfcreator)
+        table_data = tbl_data_to_table(self.data)
+        #col_widths = [3.5*cm] * len(self.data[0])
+        col_widths = [1*cm, 4*cm, 5*cm, 0.8*cm, 0.8*cm, 1*cm,
+                      6*cm, 3*cm, 0.8*cm, 0.8*cm, 1*cm, 2*cm]
+        pdf_data = [Paragraph('<b>%s - Report %s</b>'
+                              % (self.creation_dt_string, job_id),
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph(
+                    translate(
+                        '${a}<br />${b}<br />${c}<br />'
+                        'Session: ${d}<br />Level: ${e}',
+                        mapping = {'a':self.certtitle,
+                                   'b':self.deptitle,
+                                   'c':self.factitle,
+                                   'd':self.session,
+                                   'e':self.level,
+                                   }),
+                    STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+
+        pdf_data.append(Spacer(1, 40))
+        if self.signatures:
+            signaturetables = get_signature_tables(
+                self.signatures, landscape=True)
+            pdf_data.append(signaturetables[0])
+
+        pdf = creator.create_pdf(
+            pdf_data, None, self.title, self.author,
+            self.right_footer, note = self.note
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class LevelReportGenerator(grok.GlobalUtility):
+
+    title = _('Level Report')
+    grok.name('level_report')
+
+    def generate(self, site, faccode=None, depcode=None, certcode=None,
+                 session=None, level=None, author=None):
+        result = LevelReport(faccode=faccode, depcode=depcode,
+                             certcode=certcode,
+                             session=session, level=level, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class LevelReportGeneratorPage(KofaPage):
+
+    grok.context(LevelReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.handleReports')
+
+    label = _('Create level report')
+
+    @property
+    def generator_name(self):
+        for name, gen in get_generators():
+            if gen == self.context:
+                return name
+        return None
+
+    def update(self, CREATE=None, faccode_depcode_certcode=None,
+               session=None, level=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_level_values()
+        try:
+            local_roles = get_user_account(self.request).getLocalRoles()
+        except AttributeError:  # Managers have no user account
+            local_roles = {}
+        lror = local_roles.get('waeup.local.ReportsOfficer', None)
+        self._set_faccode_depcode_certcode_values(lror)
+        if not faccode_depcode_certcode:
+            self.flash(_('No certificate selected.'), type="warning")
+            return
+        if CREATE:
+            # create a new report job for students by session
+            faccode = faccode_depcode_certcode.split('_')[0]
+            depcode = faccode_depcode_certcode.split('_')[1]
+            certcode = faccode_depcode_certcode.split('_')[2]
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            if level:
+                level = int(level)
+            if session:
+                session = int(session)
+            kw = dict(
+                session=session,
+                level=level,
+                faccode=faccode,
+                depcode=depcode,
+                certcode=certcode)
+            self.flash(_('New report is being created in background'))
+            job_id = container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            grok.getSite().logger.info(
+                '%s - report %s created: %s '
+                '(faculty=%s, department=%s, certificate=%s, '
+                'session=%s, level=%s)' % (
+                ob_class, job_id, self.context.title, faccode, depcode,
+                certcode, session, level))
+            self.redirect(self.parent_url)
+            return
+        return
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(x.title, x.token) for x in vocab_terms]
+        return
+
+    #def _set_level_values(self):
+    #    vocab_terms = course_levels.by_value.values()
+    #    self.levels = sorted([(x.title, x.token) for x in vocab_terms])
+    #    return
+
+    def _set_level_values(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        studylevelsource = StudyLevelSource().factory
+        self.levels = []
+        for code in studylevelsource.getValues(None):
+            title = translate(studylevelsource.getTitle(None, code),
+                'waeup.kofa', target_language=portal_language)
+            self.levels.append((title, code))
+        return
+
+    def _set_faccode_depcode_certcode_values(self, lror):
+        faccode_depcode_certcode = []
+        faculties = grok.getSite()['faculties']
+        if lror:
+           for dep in lror:
+                faccode_depcode_certcode.append(
+                    (' All certificates -- %s, %s)'
+                     %(dep.longtitle, dep.__parent__.longtitle),
+                     '%s_%s_all'
+                     %(dep.__parent__.code, dep.code)))
+                for cert in dep.certificates.values():
+                    faccode_depcode_certcode.append(
+                        ('%s -- %s, %s)'
+                         %(cert.longtitle, dep.longtitle, dep.__parent__.longtitle),
+                         '%s_%s_%s'
+                         %(dep.__parent__.code, dep.code, cert.code)))
+        else:
+            for fac in faculties.values():
+                for dep in fac.values():
+                    faccode_depcode_certcode.append(
+                        (' All certificates -- %s, %s)'
+                         %(dep.longtitle, fac.longtitle),
+                         '%s_%s_all'
+                         %(fac.code, dep.code)))
+                    for cert in dep.certificates.values():
+                        faccode_depcode_certcode.append(
+                            ('%s -- %s, %s)'
+                             %(cert.longtitle, dep.longtitle, fac.longtitle),
+                             '%s_%s_%s'
+                             %(fac.code, dep.code, cert.code)))
+        self.faccode_depcode_certcode = sorted(
+            faccode_depcode_certcode, key=lambda value: value[0])
+        return
+
+class LevelReportPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(ILevelReport)
+    grok.name('pdf')
+    grok.require('waeup.handleReports')
+
+    def _filename(self):
+        return 'LevelReport_rno%s_%s.pdf' % (
+            self.context.__name__,
+            self.context.creation_dt_string)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/raw_score_report.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/raw_score_report.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/raw_score_report.py	(revision 16045)
@@ -0,0 +1,328 @@
+## $Id$
+##
+## Copyright (C) 2013 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
+##
+import grok
+import textwrap
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils, GRADUATED,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+from waeup.kofa.browser.pdf import get_signature_tables
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.students.reports.level_report import LevelReportGeneratorPage
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportPDFView)
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from reportlab.platypus.flowables import Flowable
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+class TTR(Flowable): #TableTextRotate
+    '''Rotates a text in a table cell.'''
+
+    def __init__(self, text):
+        Flowable.__init__(self)
+        self.text=text
+
+    def draw(self):
+        canvas = self.canv
+        canvas.rotate(25)
+        canvas.drawString( 0, -1, self.text)
+
+def tbl_data_to_table(data, certcourses):
+    col_names = (
+            #'Student Id',
+            'S/N',
+            'Matric No.',
+            'Name',
+            )
+    for certcourse in certcourses:
+        course = certcourse.course
+        if certcourse.course_category:
+            col_names += (TTR('%s (%s, %s)' % (
+                course.code, course.credits, certcourse.course_category)),)
+        else:
+            col_names += (TTR('%s (%s)' % (course.code, course.credits)),)
+    table = [col_names]
+    sn = 1
+    for line in data:
+        scores = tuple()
+        composed_line = (sn,) + line[:-1]
+        for certcourse in certcourses:
+            result = line[-1].get(certcourse.course.code)
+            if not result:
+                scores += ('N/R',)
+            else:
+                scores += ('%s %s' % (result[0], result[1]),)
+        composed_line += scores
+        table.append(composed_line)
+        sn += 1
+    return table
+
+class IRawScoreReport(IReport):
+
+    session = Attribute('Session to report')
+    level = Attribute('Level to report')
+    faccode = Attribute('Faculty to report')
+    depcode = Attribute('Department to report')
+    certcode = Attribute('Certificate to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+@implementer(IRawScoreReport)
+class RawScoreReport(Report):
+    data = None
+    session = None
+    level = None
+    faccode = None
+    depcode = None
+    certcode = None
+    note = ""
+    signatures = None
+    fontsize = 10
+
+    @property
+    def table_style(self):
+        return [
+            ('FONT', (0,0), (-1,-1), 'Helvetica', self.fontsize-2),
+            ('VALIGN', (0, 1), (-1,-1), 'TOP'),
+            #('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8), # 1st column
+            ('FONT', (0,0), (-1,0), 'Helvetica-Bold', self.fontsize-2), # 1st row
+            #('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8), # last row
+            #('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8), # last column
+            ('ALIGN', (3,1), (-1,-1), 'CENTER'),
+            ('ALIGN', (0,0), (0,-1), 'RIGHT'), # 1st column
+            #('ALIGN', (6,0), (6,-1), 'LEFT'),
+            ('INNERGRID', (0,1), (-1,-1), 0.25, colors.black),
+            ('BOX', (0,1), (-1,-1), 1, colors.black),
+            ]
+
+    @property
+    def col_width(self):
+        return [1*cm, 4*cm, 5*cm] + [1*cm] * len(self.certcourses)
+
+    @property
+    def title(self):
+        return translate(_('Raw Score Report'))
+
+    @property
+    def right_footer(self):
+        return self.title + ' - %s -' % self.session
+
+    def _excluded(self, level_obj):
+        """Some universities may add further conditions to exclude
+        students from reports. These conditions can be customized in
+        this function.
+        """
+        return False
+
+    def _get_courses(self, faccode, depcode, certcode, session, level):
+        """Method 1: Get certificate courses of a
+        certain department/certificate at a certain level.
+        """
+        site = grok.getSite()
+        certcourses = []
+        department = site['faculties'][faccode][depcode]
+        if certcode == 'all':
+            for cert in department.certificates.values():
+                for certcourse in cert.values():
+                    if certcourse.level == level:
+                        certcourses.append(certcourse)
+        else:
+            certificate = site['faculties'][faccode][depcode].certificates[certcode]
+            for certcourse in certificate.values():
+                if certcourse.level == level:
+                    certcourses.append(certcourse)
+        return certcourses
+
+    def _get_students(self, faccode, depcode, certcode, session, level, certcourses):
+        """Get students in a certain department/certificate who registered courses
+        in a certain session at a certain level.
+
+        Returns a list of student data tuples.
+        """
+        cat = queryUtility(ICatalog, name="students_catalog")
+        if certcode == 'all':
+            certcode = None
+        result = cat.searchResults(
+            depcode = (depcode, depcode), faccode = (faccode, faccode),
+            certcode = (certcode, certcode)
+            )
+        table = []
+        for stud in result:
+            if not stud['studycourse'].has_key(str(level)):
+                continue
+            if stud.state == GRADUATED:
+                continue
+            level_obj = stud['studycourse'][str(level)]
+            if level_obj.level_session != session:
+                continue
+            if self._excluded(level_obj):
+                continue
+            scores = dict()
+            for ticket in level_obj.values():
+                if ticket.code in [i.course.code for i in certcourses]:
+                    if ticket.total_score is not None:
+                        scores[ticket.code] = (ticket.total_score, ticket.grade)
+                    else:
+                        scores[ticket.code] = ('Nil', '')
+            fullname = textwrap.fill(stud.display_fullname, 26)
+            line = (
+                    stud.matric_number,
+                    fullname,
+                    scores,
+                    )
+            table.append(line)
+        return sorted(table, key=lambda value:value[0])
+
+    def __init__(self, faccode, depcode, certcode, session, level,
+                 author='System'):
+        super(RawScoreReport, self).__init__(
+            args=[faccode, depcode, certcode, session, level],
+                  kwargs={'author':author})
+        site = grok.getSite()
+        self.studylevelsource = StudyLevelSource().factory
+        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.levelcode = level
+        self.sessioncode = session
+        self.faccode = faccode
+        self.depcode = depcode
+        self.certcode = certcode
+        self.factitle = site['faculties'][faccode].longtitle
+        self.deptitle = site['faculties'][faccode][depcode].longtitle
+        if self.certcode == 'all':
+            self.certtitle = translate(
+                _('All Certificates'), 'waeup.kofa',
+                target_language=self.portal_language)
+            self.level = translate(
+                self.studylevelsource.getTitle(None, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        else:
+            certificate = site[
+                'faculties'][faccode][depcode].certificates[certcode]
+            self.certtitle = certificate.longtitle
+            self.level = translate(
+                self.studylevelsource.getTitle(certificate, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.past_levels = range(int(level/100)*100, 0, -100)
+        #self.course_codes = dict()
+        self.certcourses = dict()
+        self.data = dict()
+        for level in self.past_levels:
+            certcourses = self._get_courses(
+                faccode, depcode, certcode, session, level)
+            self.certcourses[level] = certcourses
+            self.data[level] = self._get_students(
+                faccode, depcode, certcode, session,
+                self.levelcode, certcourses)
+
+    def create_pdf(self, job_id):
+        creator = getUtility(IPDFCreator, name='A3landscape')
+        table_data = dict()
+        for level in self.past_levels:
+            table_data[level] = tbl_data_to_table(
+                self.data[level], self.certcourses[level])
+        pdf_data = [Paragraph('<font size=%s><b>%s - Report %s</b></font>'
+                              % (self.fontsize, self.creation_dt_string, job_id),
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph(
+                    translate(
+                        '<font size=${s}>${a}<br />${b}<br />${c}<br />'
+                        'Session: ${d}<br />Level: ${e}</font>',
+                        mapping = {'a':self.certtitle,
+                                   'b':self.deptitle,
+                                   'c':self.factitle,
+                                   'd':self.session,
+                                   'e':self.level,
+                                   's':self.fontsize
+                                   }),
+                    STYLE["Normal"]),
+                    ]
+
+        for level in self.past_levels:
+            pdf_data.append(Spacer(1, 20))
+            pdf_data += [Paragraph('<font size=%s><b>Level %s Courses</b></font>'
+                        % (self.fontsize, level), STYLE["Normal"]), Spacer(1, 20),]
+            pdf_data += [
+                Table(table_data[level],
+                      style=self.table_style,
+                      colWidths=self.col_width)]
+        pdf_data.append(Spacer(1, 20))
+        if self.signatures:
+            signaturetables = get_signature_tables(
+                self.signatures, landscape=True)
+            pdf_data.append(signaturetables[0])
+
+        pdf = creator.create_pdf(
+            pdf_data, None, self.title, self.author,
+            self.right_footer, note=self.note
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class RawScoreReportGenerator(grok.GlobalUtility):
+
+    title = _('RAW Score Report')
+    grok.name('raw_score_report')
+
+    def generate(self, site, faccode=None, depcode=None, certcode=None,
+                 session=None, level=None, author=None):
+        result = RawScoreReport(faccode=faccode, depcode=depcode,
+            certcode=certcode, session=session, level=level, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class RawScoreReportGeneratorPage(LevelReportGeneratorPage):
+
+    grok.context(RawScoreReportGenerator)
+
+    label = _('Create raw score report')
+
+class RawScoreReportPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(IRawScoreReport)
+    grok.name('pdf')
+    grok.require('waeup.handleReports')
+
+    def _filename(self):
+        return 'RawScoreReport_rno%s_%s.pdf' % (
+            self.context.__name__,
+            self.context.creation_dt_string)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/session_results_presentation.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/session_results_presentation.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/session_results_presentation.py	(revision 16045)
@@ -0,0 +1,324 @@
+## $Id$
+##
+## Copyright (C) 2013 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
+##
+import grok
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils, GRADUATED,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.pdf import get_signature_tables
+from waeup.kofa.reports import IReport
+from waeup.kofa.students.reports.level_report import (
+    ILevelReport, LevelReportGeneratorPage)
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportPDFView)
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from reportlab.platypus.flowables import Flowable
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8), # 1st row
+    #('ALIGN', (3,1), (-1,-1), 'RIGHT'),
+    #('ALIGN', (1,0), (1,-1), 'LEFT'),
+    #('ALIGN', (2,0), (2,-1), 'LEFT'),
+    #('INNERGRID', (0,1), (-1,-1), 0.25, colors.black),
+    #('BOX', (0,1), (-1,-1), 1, colors.black),
+    ]
+
+SUMMARY_STYLE = [
+    ('ALIGN', (3,1), (-1,-1), 'RIGHT'),
+    ]
+
+class ISessionResultsPresentation(ILevelReport):
+    """ Same interface as for level session results presentation.
+    """
+
+@implementer(ISessionResultsPresentation)
+class SessionResultsPresentation(Report):
+    data = None
+    session = None
+    level = None
+    faccode = None
+    depcode = None
+    certcode = None
+    pdfcreator = ''
+    signatures = None
+    introduction = ''
+
+    @property
+    def title(self):
+        return translate(_('Presentation of Session Results'))
+
+    @property
+    def right_footer(self):
+        return self.title + ' - %s -' % self.session
+
+    note = None
+
+    def _excluded(self, level_obj):
+        """Some universities may add further conditions to exclude
+        students from reports. These conditions can be customized in
+        this function.
+        """
+        return False
+
+    def _get_students(self, faccode, depcode, certcode, session, level=None):
+        """Get students in a certain department, studying a certain programmen
+        who registered courses in a certain session at a certain level.
+
+        Returns a list of lists of student data tuples.
+        """
+        site = grok.getSite()
+        cat = queryUtility(ICatalog, name="students_catalog")
+        if certcode == 'all':
+            certcode = None
+        result = cat.searchResults(
+            depcode = (depcode, depcode), faccode = (faccode, faccode),
+            certcode = (certcode, certcode)
+            )
+        students_utils = getUtility(IStudentsUtils)
+        table = list()
+        for i in range(len(students_utils.GPABoundaries(
+                faccode=faccode, depcode=depcode,
+                certcode=certcode))+1):
+            # The last list is reserved for students with more than one
+            # level in the same session.
+            table.append([])
+        for stud in result:
+            if stud.state == GRADUATED:
+                continue
+            line = (stud.student_id,
+                    stud.matric_number,
+                    stud.display_fullname,
+                    )
+            if level != 0:
+                if not stud['studycourse'].has_key(str(level)):
+                    continue
+                level_obj = stud['studycourse'][str(level)]
+                if level_obj.level_session != session:
+                    continue
+                if self._excluded(level_obj):
+                    continue
+            else:
+                itemcount = 0
+                for item in stud['studycourse'].values():
+                    if item.level_session == session:
+                        level_obj = item
+                        itemcount += 1
+                if itemcount == 0:
+                    # No level registered in this session
+                    continue
+                if itemcount > 1:
+                    # Error: more than one level registered in this session
+                    table[len(students_utils.GPABoundaries(
+                        faccode=faccode, depcode=depcode,
+                        certcode=certcode))].append(line)
+                    continue
+            gpaclass = students_utils.getDegreeClassNumber(level_obj)
+            table[gpaclass].append(line)
+            for i in range(len(students_utils.GPABoundaries(
+                    faccode=faccode, depcode=depcode,
+                    certcode=certcode))+1):
+                if len(table[i]):
+                    table[i] = sorted([value for value in table[i]],
+                        key=lambda value: value[2])
+        return table
+
+    def __init__(self, faccode, depcode, certcode, session, level,
+                 author='System'):
+        super(SessionResultsPresentation, self).__init__(
+            args=[faccode, depcode, certcode, session, level],
+            kwargs={'author':author})
+        site = grok.getSite()
+        self.studylevelsource = StudyLevelSource().factory
+        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.levelcode = level
+        self.sessioncode = session
+        self.faccode = faccode
+        self.depcode = depcode
+        self.certcode = certcode
+        self.factitle = site['faculties'][faccode].longtitle
+        self.deptitle = site['faculties'][faccode][depcode].longtitle
+        if self.certcode == 'all':
+            self.certtitle = translate(
+                _('All Certificates'), 'waeup.kofa',
+                target_language=self.portal_language)
+        else:
+            certificate = site[
+                'faculties'][faccode][depcode].certificates[certcode]
+            self.certtitle = certificate.longtitle
+        if level == 0:
+            self.level = translate(_('all levels'))
+        else:
+            if self.certcode == 'all':
+                self.level = translate(
+                    self.studylevelsource.getTitle(None, int(level)),
+                    'waeup.kofa', target_language=self.portal_language)
+            else:
+                self.level = translate(
+                    self.studylevelsource.getTitle(certificate, int(level)),
+                    'waeup.kofa', target_language=self.portal_language)
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = self._get_students(faccode, depcode, certcode, session, level)
+
+    def create_pdf(self, job_id):
+        creator = getUtility(IPDFCreator, name=self.pdfcreator)
+        #col_widths = [2*cm, 4*cm, 5*cm, 0.8*cm, 0.8*cm, 0.8*cm, 6*cm, ]
+        pdf_data = [Paragraph('<b>%s - Report %s</b>'
+                              % (self.creation_dt_string, job_id),
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        if self.introduction:
+            pdf_data += [Paragraph(self.introduction,
+                         STYLE["Normal"]), Spacer(1, 12)]
+        pdf_data += [Paragraph(
+                    translate(
+                        '${a}<br />${b}<br />${c}<br />Session: ${d}<br />'
+                        'Level: ${e}',
+                        mapping = {'a':self.certtitle,
+                                   'b':self.deptitle,
+                                   'c':self.factitle,
+                                   'd':self.session,
+                                   'e':self.level,
+                                   }),
+                    STYLE["Normal"]),
+                    ]
+        students_utils = getUtility(IStudentsUtils)
+        # Print classes in reverse order
+        for gpa_class in range(len(students_utils.GPABoundaries(
+                faccode=self.faccode, depcode=self.depcode,
+                certcode=self.certcode))-1,-1,-1):
+            pdf_data.append(Spacer(1, 20))
+            gpa_class_name = students_utils.GPABoundaries(
+                faccode=self.faccode, depcode=self.depcode,
+                certcode=self.certcode)[gpa_class][1]
+            pdf_data += [Paragraph('<strong>%s</strong>' % gpa_class_name,
+                         STYLE["Normal"])]
+            pdf_data.append(Spacer(1, 6))
+            if len(self.data[gpa_class]):
+                sns = range(len(self.data[gpa_class]))
+                gpa_class_data = [(i+1,) + self.data[gpa_class][i] for i in sns]
+            else:
+                gpa_class_data = [('', '', '- Nil -', '')]
+            table_data = [('S/N', 'Student Id', 'Matric No.',
+                translate(_('Student Name')))] + gpa_class_data
+            pdf_data += [Table(table_data, style=TABLE_STYLE)]    #, colWidths=col_widths)]
+        if self.data[-1]:
+            pdf_data.append(Spacer(1, 20))
+            pdf_data += [
+                Paragraph(
+                    '<strong>Students with erroneous data</strong>', STYLE["Normal"])]
+            pdf_data.append(Spacer(1, 10))
+            table_data = [('Student Id', 'Matric No.', 'Name')] + self.data[-1]
+            pdf_data += [Table(table_data, style=TABLE_STYLE)]
+        pdf_data.append(Spacer(1, 20))
+        pdf_data += [Paragraph('<strong>Summary</strong>', STYLE["Normal"])]
+        pdf_data.append(Spacer(1, 10))
+        total_count = 0
+        table_data = list()
+        for gpa_class in range(len(students_utils.GPABoundaries(
+                faccode=self.faccode, depcode=self.depcode,
+                certcode=self.certcode))-1,-1,-1):
+            gpa_class_name = students_utils.GPABoundaries(
+                faccode=self.faccode, depcode=self.depcode,
+                certcode=self.certcode)[gpa_class][1]
+            gpa_count = len(self.data[gpa_class])
+            total_count += gpa_count
+            table_data += [(gpa_class_name + ':', '%02d' % gpa_count)]
+            #pdf_data += [Paragraph('%s: %s'
+            #             % (gpa_class_name, gpa_count), STYLE["Normal"])]
+            pass
+        #pdf_data += [Paragraph('Total: %s' % (total_count), STYLE["Normal"])]
+        table_data += [('Total:', '%02d' % total_count)]
+        pdf_data += [Table(table_data, style=SUMMARY_STYLE, hAlign='LEFT')]
+
+        pdf_data.append(Spacer(1, 40))
+        if self.signatures:
+            signaturetables = get_signature_tables(self.signatures)
+            pdf_data.append(signaturetables[0])
+
+        pdf = creator.create_pdf(
+            pdf_data, None, self.title, self.author,
+            self.right_footer, note = self.note
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class SessionResultsPresentationGenerator(grok.GlobalUtility):
+
+    title = _('Session Results Presentation')
+    grok.name('session_results_presentation')
+
+    def generate(self, site, faccode=None, depcode=None, certcode=None,
+                 session=None, level=None, author=None):
+        result = SessionResultsPresentation(faccode=faccode, depcode=depcode,
+                 certcode=certcode, session=session, level=level, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class SessionResultsPresentationGeneratorPage(LevelReportGeneratorPage):
+
+    grok.context(SessionResultsPresentationGenerator)
+    grok.template('levelreportgeneratorpage')
+    label = _('Create session results presentation')
+
+    def _set_level_values(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        studylevelsource = StudyLevelSource().factory
+        self.levels = [(u'All levels', 0)]
+        for code in studylevelsource.getValues(None):
+            title = translate(studylevelsource.getTitle(None, code),
+                'waeup.kofa', target_language=portal_language)
+            self.levels.append((title, code))
+        return
+
+class SessionResultsPresentationPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(ISessionResultsPresentation)
+    grok.name('pdf')
+    grok.require('waeup.handleReports')
+
+    def _filename(self):
+        return 'SessionResultsPresentation_rno%s_%s.pdf' % (
+            self.context.__name__,
+            self.context.creation_dt_string)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_payment_statistics.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_payment_statistics.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_payment_statistics.py	(revision 16045)
@@ -0,0 +1,364 @@
+## $Id$
+##
+## Copyright (C) 2015 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
+##
+import grok
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+
+
+class IStudentPaymentStatisticsReport(IReport):
+
+    session = Attribute('Session to report')
+    mode = Attribute('Study modes group to report')
+    level = Attribute('Level to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+
+def get_student_payment_stats(session, mode, level, entry_session,
+        p_session, breakdown):
+    """Get payment amounts of students in a certain session, study mode
+    and current level.
+
+    Returns a table ordered by code (faculty or department, one per row) and
+    various payment categories (cols). The result is a 3-tuple representing
+    ((<CODES>), (<PAYMENTCATS>), (<NUM_OF_STUDENTS>)). The
+    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
+    number of students found in that faculty/department and with the respective
+    payment.
+
+    Sample result:
+
+      >>> ((u'FAC1', u'FAC2'),
+      ...  ('clearance', 'gown', 'hostel_maintenance', 'schoolfee'),
+      ...  ((12, 10, 1, 14), (0, 5, 25, 16)))
+
+    This result means: there are 5 students in FAC2 in who have paid
+    gown fee while 12 students in 'FAC1' who have paid for clearance.
+    """
+    site = grok.getSite()
+    payment_cats_dict = getUtility(IKofaUtils).REPORTABLE_PAYMENT_CATEGORIES
+    payment_cats = tuple(sorted(payment_cats_dict.keys())) + ('Total',)
+    if breakdown == 'faccode':
+        paths = tuple(sorted([x for x in site['faculties'].keys()],
+                                 key=lambda x: x.lower()))
+    elif breakdown == 'depcode':
+        faculties = site['faculties']
+        deppaths = []
+        for fac in faculties.values():
+            for dep in fac.values():
+                deppaths.append('%s/%s' % (fac.code, dep.code))
+        paths = tuple(sorted([x for x in deppaths],
+                                 key=lambda x: x.lower()))
+    # XXX: Here we do _one_ query and then examine the single
+    #   students. One could also do multiple queries and just look for
+    #   the result length (not introspecting the delivered students
+    #   further).
+    cat = queryUtility(ICatalog, name="students_catalog")
+    if session == 0:
+        result = cat.searchResults(student_id=(None, None))
+    else:
+        result = cat.searchResults(current_session=(session, session))
+    table = [[0 for x in xrange(2*len(payment_cats))]
+                for y in xrange(len(paths)+1)]
+    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
+    for stud in result:
+        if mode != 'All' and stud.current_mode \
+            and stud.current_mode not in mode_groups[mode]:
+            continue
+        if level != 0 and stud.current_level != level:
+            continue
+        if entry_session != 0 and  stud.entry_session != entry_session:
+            continue
+        if None in  (getattr(stud, 'faccode'), getattr(stud, 'depcode')):
+            continue
+        # Dep codes are not unique.
+        if breakdown == 'depcode':
+            row = paths.index(
+                '%s/%s' % (getattr(stud, 'faccode'), getattr(stud, 'depcode')))
+        else:
+            row = paths.index(getattr(stud, breakdown))
+        for key in stud['payments'].keys():
+            ticket = stud['payments'][key]
+            if ticket.p_category not in payment_cats:
+                continue
+            if ticket.p_state != 'paid':
+                continue
+            if p_session != 0 and ticket.p_session != p_session:
+                continue
+            col = payment_cats.index(ticket.p_category)
+
+            # payments in category and fac/dep
+            table[row][2*col] += 1 # number of payments
+            table[row][2*col+1] += ticket.amount_auth
+
+            # payments in category
+            table[-1][2*col] += 1
+            table[-1][2*col+1] += ticket.amount_auth
+
+            # payments in fac/dep
+            table[row][-2] += 1
+            table[row][-1] += ticket.amount_auth
+
+            # all payments
+            table[-1][-2] += 1
+            table[-1][-1] += ticket.amount_auth
+
+    # Build table header
+    table_header = ['' for x in xrange(len(payment_cats))]
+    for cat in payment_cats:
+        cat_title = payment_cats_dict.get(cat, cat)
+        table_header[payment_cats.index(cat)] = cat_title
+
+    # Turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+
+    paths = paths + (u'Total',)
+    return (paths, table_header, table)
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+from waeup.kofa.students.reports.level_report import TTR
+
+from waeup.kofa.students.reports.student_statistics import (
+    tbl_data_to_table, STYLE)
+
+def tbl_data_to_table(row_names, col_names, data):
+    result = []
+    new_col_names = []
+    for name in col_names:
+        new_col_names.append(name.replace(' ', '\n'))
+    head = [''] + [col_name for col_name in new_col_names]
+    result = [head]
+    for idx, row_name in enumerate(row_names):
+        row = []
+        i = 0
+        while i < len(data[idx]):
+            if row_name == 'Total':
+                row.append(
+                    "%s\n(%s)" % ("{:,}".format(data[idx][i+1]), data[idx][i]))
+            else:
+                row.append(
+                    "%s (%s)" % ("{:,}".format(data[idx][i+1]), data[idx][i]))
+            i += 2
+        row = [row_name] + row
+        result.append(row)
+    return result
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
+    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
+    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
+    ('ALIGN', (0,0), (-1,-1), 'RIGHT'),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,0), (-1,-1), 0.25, colors.black),
+    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IStudentPaymentStatisticsReport)
+class StudentPaymentStatisticsReport(Report):
+    data = None
+    session = None
+    mode = None
+    entry_session = None
+    p_session = None
+    pdfcreator = 'landscape'
+    title = translate(_('Student Payment Statistics'))
+
+    @property
+    def title(self):
+        return translate(_('Student Payment Statistics'))
+
+    def __init__(self, session, mode, level, entry_session, p_session,
+                 breakdown, author='System'):
+        super(StudentPaymentStatisticsReport, self).__init__(
+            args=[session, mode, level, entry_session, p_session, breakdown],
+            kwargs={'author':author})
+        self.sessioncode = session
+        self.levelcode = level
+        self.entrysessioncode = entry_session
+        self.psessioncode = p_session
+        self.studylevelsource = StudyLevelSource().factory
+        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        if session != 0:
+            self.session = academic_sessions_vocab.getTerm(session).title
+        else:
+            self.session = 'All sessions'
+        if entry_session != 0:
+            self.entry_session = academic_sessions_vocab.getTerm(
+                entry_session).title
+        else:
+            self.entry_session = 'All sessions'
+        if p_session != 0:
+            self.p_session = academic_sessions_vocab.getTerm(
+                p_session).title
+        else:
+            self.p_session = 'All sessions'
+        if mode == 'All':
+            self.mode = 'All study modes'
+        else:
+            self.mode = mode
+        if level == 0:
+            self.level = 'All levels'
+        else:
+            self.level = translate(
+                self.studylevelsource.getTitle(None, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        self.breakdown = breakdown
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = get_student_payment_stats(
+            session, mode, level, entry_session, p_session, breakdown)
+
+    def create_pdf(self, job_id):
+        creator = getUtility(IPDFCreator, name=self.pdfcreator)
+        # maxlength = len(str(self.data[2][-1][-1])) \
+        #     + len(str(self.data[2][-1][-2])) \
+        #     + 3
+        # maxlength = max(maxlength, 10)
+        table_data = tbl_data_to_table(*self.data)
+        # col_widths = [None,] + [.17*cm*maxlength] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>%s - Report %s</b>'
+                              % (self.creation_dt_string, job_id),
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph(
+                    translate(
+                        'Study Mode: ${a}<br />'
+                        'Academic Session: ${b}<br />Level: ${c}<br />'
+                        'Entry Session: ${d}<br />Payment Session: ${e}',
+                        mapping = {'a':self.mode,
+                                   'b':self.session,
+                                   'c':self.level,
+                                   'd':self.entry_session,
+                                   'e':self.p_session,
+                                   }),
+                    STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE)]
+        right_footer = translate(
+            _('Student Payments - ${a} - ${b} - ${c} - ',
+            mapping = {'a':self.mode, 'b':self.session, 'c':self.level}))
+        pdf = creator.create_pdf(
+            pdf_data, None, self.title, self.author, right_footer
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class StudentPaymentStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Student Payment Statistics')
+    grok.name('student_payment_stats')
+
+    def generate(
+        self, site, session=None, mode=None,
+        level=None, entry_session=None, p_session=None,
+        breakdown=None, author=None):
+        result = StudentPaymentStatisticsReport(
+            session=session, mode=mode, level=level,
+            entry_session=entry_session, p_session=p_session,
+            breakdown=breakdown, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportGeneratorPage,
+    StudentStatisticsReportPDFView)
+
+class StudentPaymentStatisticsReportGeneratorPage(
+    StudentStatisticsReportGeneratorPage):
+
+    grok.context(StudentPaymentStatisticsReportGenerator)
+    grok.template('studentpaymentstatisticsreportgeneratorpage')
+
+    label = _('Create student payment statistics report')
+
+    def update(
+        self, CREATE=None, session=None, mode=None, level=None,
+        entry_session=None, p_session=None, breakdown=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_mode_values()
+        self._set_level_values()
+        self._set_breakdown_values()
+        if CREATE and session:
+            # create a new report job for students by session and level
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict(
+                session=int(session),
+                mode=mode,
+                level=int(level),
+                entry_session=int(entry_session),
+                p_session=int(p_session),
+                breakdown=breakdown)
+            self.flash(_('New report is being created in background'))
+            job_id = container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            grok.getSite().logger.info(
+                '%s - report %s created: %s (session=%s, mode=%s, level=%s, '
+                'entry_session=%s, p_session=%s, breakdown=%s)' % (
+                ob_class, job_id, self.context.title,
+                session, mode, level, entry_session, p_session, breakdown))
+            self.redirect(self.parent_url)
+            return
+        return
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(x.title, x.token) for x in vocab_terms]
+        self.sessions_plus = [(u'All', 0)] + self.sessions
+        return
+
+class StudentPaymentStatisticsReportPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(IStudentPaymentStatisticsReport)
+
+    def _filename(self):
+        return 'StudentPaymentStatisticsReport_rno%s_%s.pdf' % (
+            self.context.__name__,
+            self.context.creation_dt_string)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_statistics.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_statistics.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/student_statistics.py	(revision 16045)
@@ -0,0 +1,315 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+import grok
+from zope.i18n import translate
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+
+class IStudentStatisticsReport(IReport):
+
+    session = Attribute('Session to report')
+    level = Attribute('Level to report')
+    mode = Attribute('Study modes group to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+def get_student_stats(session, mode, level, breakdown):
+    """Get students in a certain session, study mode and current level.
+
+    Returns a table ordered by code (faculty or department, one per row) and
+    registration state (cols). The result is a 3-tuple representing
+    ((<PATHS>), (<STATES>), (<NUM_OF_STUDENTS>)). The
+    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
+    number of students found in that faculty/department and with the respective
+    state.
+
+    Sample result:
+
+      >>> ((u'FAC1', u'FAC2'),
+      ...  ('created', 'accepted', 'registered'),
+      ...  ((12, 10, 1), (0, 5, 25)))
+
+    This result means: there are 5 students in FAC2 in state
+    'accepted' while 12 students in 'FAC1' are in state 'created'.
+    """
+    site = grok.getSite()
+    states = tuple([x.value for x in registration_states_vocab])
+    states = states + (u'Total',)
+    if breakdown == 'faccode':
+        paths = tuple(sorted([x for x in site['faculties'].keys()],
+                                 key=lambda x: x.lower()))
+    elif breakdown == 'depcode':
+        faculties = site['faculties']
+        deppaths = []
+        for fac in faculties.values():
+            for dep in fac.values():
+                deppaths.append('%s/%s' % (fac.code, dep.code))
+        paths = tuple(sorted([x for x in deppaths],
+                                 key=lambda x: x.lower()))
+    # XXX: Here we do _one_ query and then examine the single
+    #   students. One could also do multiple queries and just look for
+    #   the result length (not introspecting the delivered students
+    #   further).
+    cat = queryUtility(ICatalog, name="students_catalog")
+    result = cat.searchResults(current_session=(session, session))
+    table = [[0 for x in xrange(len(states))] for y in xrange(len(paths)+1)]
+    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
+    for stud in result:
+        if mode != 'All' and stud.current_mode \
+            and stud.current_mode not in mode_groups[mode]:
+            continue
+        if level != 0 and stud.current_level != level:
+            continue
+        if getattr(stud, breakdown) is None:
+            continue
+        if breakdown == 'faccode':
+            row_name = getattr(stud, 'faccode')
+        elif breakdown == 'depcode':
+            row_name = '%s/%s' % (
+                getattr(stud, 'faccode'), getattr(stud, 'depcode'))
+        row = paths.index(row_name)
+        col = states.index(stud.state)
+        table[row][col] += 1
+        table[-1][col] += 1
+        table[row][-1] += 1
+        table[-1][-1] += 1
+    # turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+
+    paths = paths + (u'Total',)
+    return (paths, states, table)
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+def tbl_data_to_table(row_names, col_names, data):
+    result = []
+    new_col_names = []
+    for name in col_names:
+        new_col_names.append(name.replace(' ', '\n'))
+    head = [''] + list(new_col_names)
+    result = [head]
+    for idx, row_name in enumerate(row_names):
+        row = [row_name] + list(data[idx])
+        result.append(row)
+    return result
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
+    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
+    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
+    ('ALIGN', (1,1), (-1,-1), 'RIGHT'),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,0), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IStudentStatisticsReport)
+class StudentStatisticsReport(Report):
+    data = None
+    session = None
+    mode = None
+    pdfcreator = 'landscape'
+    title = translate(_('Student Statistics'))
+
+    @property
+    def title(self):
+        return translate(_('Student Statistics'))
+
+    def __init__(self, session, mode, level, breakdown, author='System'):
+        super(StudentStatisticsReport, self).__init__(
+            args=[session, mode, breakdown], kwargs={'author':author})
+        self.sessioncode = session
+        self.levelcode = level
+        self.studylevelsource = StudyLevelSource().factory
+        self.portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.mode = mode
+        if level == 0:
+            self.level = 'All levels'
+        else:
+            self.level = translate(
+                self.studylevelsource.getTitle(None, int(level)),
+                'waeup.kofa', target_language=self.portal_language)
+        self.breakdown = breakdown
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = get_student_stats(session, mode, level, breakdown)
+
+    def create_pdf(self, job_id):
+        creator = getUtility(IPDFCreator, name=self.pdfcreator)
+        table_data = tbl_data_to_table(*self.data)
+        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>%s - Report %s</b>'
+                              % (self.creation_dt_string, job_id),
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph(
+                    translate(
+                        'Study Mode: ${a}<br />'
+                        'Academic Session: ${b}<br />Level: ${c}<br />',
+                        mapping = {'a':self.mode,
+                                   'b':self.session,
+                                   'c':self.level,
+                                   }),
+                    STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        right_footer = translate(
+            _('${a} Students - ${b} -',
+            mapping = {'a':self.mode, 'b':self.session}))
+        pdf = creator.create_pdf(
+            pdf_data, None, self.title, self.author, right_footer
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class StudentStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Student Statistics')
+    grok.name('student_stats')
+
+    def generate(
+        self, site, session=None, mode=None,
+        level=None, breakdown=None, author=None):
+        result = StudentStatisticsReport(
+            session=session, mode=mode, level=level,
+            breakdown=breakdown, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class StudentStatisticsReportGeneratorPage(KofaPage):
+
+    grok.context(StudentStatisticsReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.handleReports')
+
+    label = _('Create student statistics report')
+
+    @property
+    def generator_name(self):
+        for name, gen in get_generators():
+            if gen == self.context:
+                return name
+        return None
+
+    def update(
+        self, CREATE=None, session=None, mode=None, level=None, breakdown=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_mode_values()
+        self._set_level_values()
+        self._set_breakdown_values()
+        if CREATE and session:
+            # create a new report job for students by session and level
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict(
+                session=int(session),
+                mode=mode,
+                level=int(level),
+                breakdown=breakdown)
+            self.flash(_('New report is being created in background'))
+            job_id = container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            grok.getSite().logger.info(
+                '%s - report %s created: %s '
+                '(session=%s, mode=%s, level=%s, breakdown=%s)' % (
+                ob_class, job_id, self.context.title,
+                session, mode, level, breakdown))
+            self.redirect(self.parent_url)
+            return
+        return
+
+    def _set_session_values(self):
+        vocab_terms = academic_sessions_vocab.by_value.values()
+        self.sessions = [(x.title, x.token) for x in vocab_terms]
+        return
+
+    def _set_mode_values(self):
+        mode_groups = getUtility(IKofaUtils).MODE_GROUPS
+        self.modes = sorted([(key, key) for key in mode_groups.keys()])
+        return
+
+    def _set_level_values(self):
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        studylevelsource = StudyLevelSource().factory
+        self.levels = [(u'All', 0)]
+        for code in studylevelsource.getValues(None):
+            title = translate(studylevelsource.getTitle(None, code),
+                'waeup.kofa', target_language=portal_language)
+            self.levels.append((title, code))
+        return
+
+    def _set_breakdown_values(self):
+        self.breakdowns = [(_('Faculties'), 'faccode'), (_('Departments'), 'depcode')]
+        return
+
+class StudentStatisticsReportPDFView(grok.View):
+
+    grok.context(IStudentStatisticsReport)
+    grok.name('pdf')
+    grok.require('waeup.handleReports')
+
+    def _filename(self):
+        return 'StudentStatisticsReport_rno%s_%s.pdf' % (
+            self.context.__name__,
+            self.context.creation_dt_string)
+
+    def render(self):
+        filename = self._filename().replace(
+            '/', '_').replace(' ','_').replace(':', '-')
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        pdf_stream = self.context.create_pdf(self.context.__name__)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        grok.getSite().logger.info('%s - report %s downloaded: %s' % (
+            ob_class, self.context.__name__, filename))
+        return pdf_stream
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_level_report.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_level_report.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_level_report.py	(revision 16045)
@@ -0,0 +1,189 @@
+import os
+from zc.async.testing import wait_for_result
+from zope.event import notify
+from zope.interface.verify import verifyClass, verifyObject
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.component import getUtility, createObject
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.students.reports.level_report import (
+    LevelReport, ILevelReport)
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.tests.test_authentication import SECRET
+
+class LevelReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = LevelReport('fac1', 'dep1', 'CERT1', 2010, 100)
+        verifyClass(ILevelReport, LevelReport)
+        verifyObject(ILevelReport, obj)
+        return
+
+        # Credits counted (taken): 90
+        # Credits passed: 60
+        # Credits failed: 30
+        # Credits registered (total_credits): 180
+        # Course1 and Course3 are passed
+        # Course2 is failed
+        # Course4, Course5 and Course6 have not ben taken
+
+    def _add_tickets(self):
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course2'
+        ticket.credits = 30
+        ticket.score = 30
+        ticket.mandatory = True
+        ticket.passmark = 40
+        self.student['studycourse']['100']['Course2'] = ticket
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course3'
+        ticket.credits = 30
+        ticket.score = 30
+        ticket.mandatory = False
+        ticket.passmark = 40
+        self.student['studycourse']['100']['Course3'] = ticket
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course4'
+        ticket.credits = 30
+        ticket.mandatory = False
+        ticket.passmark = 40
+        self.student['studycourse']['100']['Course4'] = ticket
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course5'
+        ticket.credits = 30
+        self.student['studycourse']['100']['Course5'] = ticket
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course6'
+        ticket.credits = 30
+        self.student['studycourse']['100']['Course6'] = ticket
+        return
+
+    def test_get_students(self):
+        self._add_tickets()
+         # we can get a table with one student
+        lr =  LevelReport('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = lr._get_students('fac1', 'dep1', 'CERT1', 2010, 100)
+        self.assertEqual(result,
+            [(u'1234', u'Bob Tester', 90, 30, '1.666',
+            'm_Course2_m Course3', 'Course4 Course5\nCourse6',
+            90, 30, '1.666', '')])
+        # same result when selecting all certificates
+        result = lr._get_students('fac1', 'dep1', 'all', 2010, 100)
+        self.assertEqual(result,
+            [(u'1234', u'Bob Tester', 90, 30, '1.666',
+            'm_Course2_m Course3', 'Course4 Course5\nCourse6',
+            90, 30, '1.666', '')])
+        return
+
+    def test_create_pdf(self):
+        self.student.firstname = u'Johnathan Emmanuel Amenaghawon'
+        self.student.lastname = u'Ehissouria'
+        self.student['studycourse']['100']['Course1'].passmark = 40
+        self._add_tickets()
+        report = LevelReport('fac1', 'dep1', 'all', 2010, 100)
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'level_report.pdf')
+        open(path, 'wb').write(result)
+        print "Sample level_report.pdf written to %s" % path
+        return
+
+class LevelReportUITests(StudentsFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self):
+        #self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # Create reports officer
+        self.app['users'].addUser('mrreport', SECRET)
+        self.app['users']['mrreport'].email = 'mrreport@foo.ng'
+        self.app['users']['mrreport'].title = 'Carlo Pitter'
+        # Assign global reports officer role
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.ReportsOfficer', 'mrreport')
+        # Assign local ReportsOfficer role which restricts the access to
+        # certain faculties.
+        self.department = self.app['faculties']['fac1']['dep1']
+        prmlocal = IPrincipalRoleManager(self.department)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.ReportsOfficer', 'mrreport')
+        notify(LocalRoleSetEvent(
+            self.department, 'waeup.local.ReportsOfficer', 'mrreport',
+            granted=True))
+        # Login as reports officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrreport'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['level_report']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="session").value = ['2010']
+        self.browser.getControl(name="faccode_depcode_certcode").value = [
+            'fac1_dep1_CERT1']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="LevelReport_rno%s' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - mrreport - students.reports.level_report.LevelReportGeneratorPage - '
+            'report %s created: Level Report (faculty=fac1, department=dep1, '
+            'certificate=CERT1, session=2010, level=100)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - mrreport - students.reports.level_report.LevelReportPDFView - '
+            'report %s downloaded: LevelReport_rno%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'INFO - mrreport - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_raw_score_report.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_raw_score_report.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_raw_score_report.py	(revision 16045)
@@ -0,0 +1,90 @@
+import os
+from zc.async.testing import wait_for_result
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility, createObject
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.students.reports.raw_score_report import (
+    RawScoreReport, IRawScoreReport)
+
+
+class RawScoreReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(RawScoreReportTests, self).setUp()
+        self.course = createObject('waeup.Course')
+        self.course.code = 'Course1'
+        self.course.credits = 25
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        self.app['faculties']['fac1']['dep1'].certificates[
+            'CERT1'].addCertCourse(self.course, level=100)
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = RawScoreReport('fac1', 'dep1', 'CERT1', 2010, 100)
+        verifyClass(IRawScoreReport, RawScoreReport)
+        verifyObject(IRawScoreReport, obj)
+        return
+
+    def test_get_courses(self):
+        # we can get a list with one course code
+        rsr = RawScoreReport('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = rsr._get_courses('fac1', 'dep1', 'CERT1', 2010, 100)
+        self.assertEqual(result, [self.certificate.values()[0]])
+        self.assertEqual(
+            result,
+            [self.app['faculties']['fac1']['dep1'].certificates[
+                'CERT1']['Course1_100']])
+        return
+
+    def test_get_students(self):
+        # we can get a table with one student
+        rsr = RawScoreReport('fac1', 'dep1', 'CERT1', 2010, 100)
+        course_codes = rsr._get_courses('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = rsr._get_students(
+            'fac1', 'dep1', 'CERT1', 2010, 100, course_codes)
+        self.assertEqual(result,
+            [(u'1234', u'Bob Tester', {'Course1': (70, 'A')})])
+        self.student['studycourse']['100']['Course1'].score = None
+        result = rsr._get_students(
+            'fac1', 'dep1', 'CERT1', 2010, 100, course_codes)
+        self.assertEqual(result,
+            [(u'1234', u'Bob Tester', {'Course1': ('Nil', '')})])
+        return
+
+    def test_create_pdf(self):
+        self.app['faculties']['fac1']['dep1'].certificates[
+                'CERT1']['Course1_100'].course_category = 'xyz'
+        self.course = createObject('waeup.Course')
+        self.course.code = 'Course2'
+        self.course.credits = 30
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        self.app['faculties']['fac1']['dep1'].certificates[
+            'CERT1'].addCertCourse(self.course, level=100)
+        studylevel = createObject('waeup.StudentStudyLevel')
+        studylevel.level = 200
+        studylevel.level_session = 2011
+        self.student['studycourse']['200'] = studylevel
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course2'
+        ticket.credits = 30
+        ticket.score = 50
+        ticket.ca = 10
+        self.student['studycourse']['200']['Course2'] = ticket
+        self.student.firstname = u'Osahenokese Tessy'
+        self.student.lastname = u'Emwinyomwanru'
+        report = RawScoreReport('fac1', 'dep1', 'CERT1', 2011, 200)
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'raw_score_report.pdf')
+        open(path, 'wb').write(result)
+        print "Sample raw_score_report.pdf written to %s" % path
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py	(revision 16045)
@@ -0,0 +1,233 @@
+import os
+from zc.async.testing import wait_for_result
+from zope.event import notify
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.students.reports.session_results_presentation import (
+    SessionResultsPresentation, ISessionResultsPresentation)
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+
+class SessionResultsPresentationTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        verifyClass(ISessionResultsPresentation, SessionResultsPresentation)
+        verifyObject(ISessionResultsPresentation, obj)
+        return
+
+    def test_get_students(self):
+        sr = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'CERT1', 2010, 100)
+        self.assertEqual(result,
+            [[], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')], []])
+        return
+
+    def test_get_students_at_all_levels(self):
+        sr = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'CERT1', 2010, 0)
+        self.assertEqual(result,
+            [[], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')], []])
+        return
+
+    def test_get_students_all_certs(self):
+        sr = SessionResultsPresentation('fac1', 'dep1', 'all', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'all', 2010, 100)
+        self.assertEqual(result,
+            [[], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')], []])
+        return
+
+    def test_get_students_with_two_levels_in_session(self):
+        # Register second level in the same session (which shouldn't be!)
+        studylevel = StudentStudyLevel()
+        studylevel.level = 200
+        studylevel.level_session = 2010
+        self.student['studycourse']['200'] = studylevel
+        sr = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'CERT1', 2010, 0)
+        self.assertEqual(result,
+            [[], [], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')]])
+        return
+
+    def test_get_students_without_scores(self):
+        self.student['studycourse']['100']['Course1'].score = None
+        sr = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'CERT1', 2010, 0)
+        self.assertEqual(result,
+            [[], [], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')]])
+
+    def test_get_students_without_level_in_session(self):
+        self.student['studycourse']['100'].level_session = 2011
+        sr = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = sr._get_students('fac1', 'dep1', 'CERT1', 2010)
+        self.assertEqual(result,
+            [[], [], [], [], [], [], []])
+        return
+
+    def test_create_pdf(self):
+        report = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 100)
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'session_results_presentation.pdf')
+        open(path, 'wb').write(result)
+        print "Sample session_results_presentation.pdf written to %s" % path
+        return
+
+    def test_create_pdf_620(self):
+        studylevel = StudentStudyLevel()
+        studylevel.level = 620
+        studylevel.level_session = 2013
+        ticket = CourseTicket()
+        ticket.code = 'Course2'
+        ticket.credits = 40
+        ticket.score = 44
+        self.student['studycourse']['620'] = studylevel
+        self.student['studycourse']['620']['Course2'] = ticket
+        report = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2013, 620)
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'session_results_presentation_620.pdf')
+        open(path, 'wb').write(result)
+        print "Sample session_results_presentation_620.pdf written to %s" % path
+        return
+
+    def test_create_pdf_with_two_levels_in_session(self):
+        # Register second level with tickets in the same session (which shouldn't be!)
+        # Check the pdf file if the student record has really been classified
+        # as erroneous.
+        studylevel = StudentStudyLevel()
+        studylevel.level = 200
+        studylevel.level_session = 2010
+        ticket = CourseTicket()
+        ticket.code = 'Course2'
+        ticket.credits = 40
+        ticket.score = 11
+        self.student['studycourse']['200'] = studylevel
+        self.student['studycourse']['200']['Course2'] = ticket
+        report = SessionResultsPresentation('fac1', 'dep1', 'CERT1', 2010, 0)
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'session_results_presentation_erroneous.pdf')
+        open(path, 'wb').write(result)
+        print "Sample session_results_presentation_erroneous.pdf written to %s" % path
+        return
+
+class SessionResultsPresentationUITests(StudentsFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self, level='100'):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['session_results_presentation']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="level").value = [level]
+        self.browser.getControl(name="session").value = ['2010']
+        self.browser.getControl(name="faccode_depcode_certcode").value = ['fac1_dep1_CERT1']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="SessionResultsPresentation_rno%s' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationGeneratorPage - '
+            'report %s created: Session Results Presentation '
+            '(faculty=fac1, department=dep1, certificate=CERT1, session=2010, level=100)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationPDFView - '
+            'report %s downloaded: SessionResultsPresentation_rno%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+
+        return
+
+    def test_report_download_all_levels(self):
+        # We can download a generated report for all levels
+        self.trigger_report_creation(level='0')
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="SessionResultsPresentation_rno%s' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationGeneratorPage - '
+            'report %s created: Session Results Presentation '
+            '(faculty=fac1, department=dep1, certificate=CERT1, session=2010, level=0)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationPDFView - '
+            'report %s downloaded: SessionResultsPresentation_rno%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py	(revision 16045)
@@ -0,0 +1,145 @@
+# Tests for student related reports
+import os
+from zc.async.testing import wait_for_result
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.students.reports.student_payment_statistics import (
+    get_student_payment_stats,
+    StudentPaymentStatisticsReport,
+    IStudentPaymentStatisticsReport)
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+
+class StudentPaymentStatisticsReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = StudentPaymentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 0, 0, 0, 'faccode')
+        verifyClass(IStudentPaymentStatisticsReport, StudentPaymentStatisticsReport)
+        verifyObject(IStudentPaymentStatisticsReport, obj)
+        return
+
+    def test_get_student_payment_stats_session_simple(self):
+        # we can get a table with one student
+        result1 = get_student_payment_stats(
+            2010, 'Undergraduate Full-Time', 0, 0, 0, 'faccode')
+        result2 = get_student_payment_stats(
+            2009, 'Undergraduate Full-Time', 0, 0, 0, 'faccode')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'Total'),
+             ['Acceptance Fee', 'Gown Hire Fee',
+              'Hostel Maintenance Fee', 'School Fee',
+              'Total'],
+              ((0, 0, 0, 0, 0, 0, 1, 12345.678, 1, 12345.678),
+              (0, 0, 0, 0, 0, 0, 1, 12345.678, 1, 12345.678)))
+            )
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'Total'),
+             ['Acceptance Fee', 'Gown Hire Fee',
+              'Hostel Maintenance Fee', 'School Fee',
+              'Total'],
+              ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))
+            )
+        return
+
+    def test_create_pdf_1(self):
+        report = StudentPaymentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 0, 0, 0, 'faccode')
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'student_payment_statistics_faccode.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF student_statistics_faccode.pdf written to %s" % path
+        return
+
+    def test_create_pdf_2(self):
+        report = StudentPaymentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 0, 0, 0, 'depcode')
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'student_payment_statistics_depcode.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF student_statistics_depcode.pdf written to %s" % path
+        return
+
+class StudentPaymentStatisticsReportUITests(StudentsFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['student_payment_stats']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="breakdown").value = ['depcode']
+        self.browser.getControl(name="mode").value = ['All']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="StudentPaymentStatisticsReport_rno%s' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_payment_statistics.StudentPaymentStatisticsReportGeneratorPage - '
+            'report %s created: Student Payment Statistics '
+            '(session=2004, mode=All, level=0, entry_session=0, '
+            'p_session=0, breakdown=depcode)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_payment_statistics.StudentPaymentStatisticsReportPDFView - '
+            'report %s downloaded: StudentPaymentStatisticsReport_rno%s_'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_statistics.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_statistics.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/reports/tests/test_student_statistics.py	(revision 16045)
@@ -0,0 +1,231 @@
+# Tests for student related reports
+import os
+from datetime import datetime, timedelta
+from zc.async.testing import wait_for_result
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility
+from waeup.kofa.interfaces import IJobManager, IKofaUtils
+from waeup.kofa.students.reports.student_statistics import (
+    get_student_stats, StudentStatisticsReport, IStudentStatisticsReport)
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+
+class StudentStatisticsReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    states = ('created', 'admitted', 'clearance started',
+              'clearance requested', 'cleared', 'school fee paid',
+              'courses registered', 'courses validated', 'returning',
+              'graduated', 'transcript requested', 'transcript validated', 
+              'transcript released', 'Total')
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = StudentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 0, 'faccode')
+        verifyClass(IStudentStatisticsReport, StudentStatisticsReport)
+        verifyObject(IStudentStatisticsReport, obj)
+        return
+
+    def test_get_student_stats_session_simple(self):
+        # we can get a table with one student
+        result1 = get_student_stats(
+            2010, 'Undergraduate Full-Time', 0, 'faccode')
+        result2 = get_student_stats(
+            2009, 'Undergraduate Full-Time', 0, 'faccode')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'Total',),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_get_student_stats_session_multiple(self):
+        # we can get a table with several students
+        self.create_cert(u'fac2', u'dep2', u'CERT2')
+        result1 = get_student_stats(
+            2010, 'Undergraduate Full-Time', 0, 'faccode')
+        result2 = get_student_stats(
+            2009, 'Undergraduate Full-Time', 0, 'faccode')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'fac2', u'Total'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'fac2', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_get_student_stats_session_multiple_dep_breakdown(self):
+        # we can get a table with several students
+        self.create_cert(u'fac2', u'dep2', u'CERT2')
+        result1 = get_student_stats(2010, 'Undergraduate Full-Time', 0, 'depcode')
+        result2 = get_student_stats(2009, 'Undergraduate Full-Time', 0, 'depcode')
+        self.assertEqual(
+            result1,
+            ((u'fac1/dep1', u'fac2/dep2', u'Total'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1/dep1', u'fac2/dep2', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_get_student_stats_session_multiple_dep_breakdown_level_100(self):
+        # we can get a table with several students
+        self.create_cert(u'fac2', u'dep2', u'CERT2')
+        result1 = get_student_stats(2010, 'Undergraduate Full-Time', 0, 'depcode')
+        result2 = get_student_stats(2009, 'Undergraduate Full-Time', 0, 'depcode')
+        self.assertEqual(
+            result1,
+            ((u'fac1/dep1', u'fac2/dep2', u'Total'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),)))
+        self.assertEqual(
+            result2,
+            ((u'fac1/dep1', u'fac2/dep2', u'Total'),
+             self.states,
+             ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),)))
+        return
+
+    def test_create_pdf(self):
+        self.create_cert(u'FAC2', u'dept2', u'CERT2')
+        report = StudentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 0, 'faccode')
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'student_statistics.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF student_statistics.pdf written to %s" % path
+        return
+
+    def test_create_100level_pdf(self):
+        self.create_cert(u'FAC2', u'dept2', u'CERT2')
+        report = StudentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 100, 'faccode')
+        result = report.create_pdf('JOB_ID')
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'student_statistics_100.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF student_statistics_100.pdf written to %s" % path
+        return
+
+class StudentStatisticsReportUITests(StudentsFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['student_stats']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="breakdown").value = ['depcode']
+        self.browser.getControl(name="mode").value = ['All']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="StudentStatisticsReport_rno%s' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_statistics.StudentStatisticsReportGeneratorPage - '
+            'report %s created: Student Statistics (session=2004, mode=All, level=0, breakdown=depcode)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_statistics.StudentStatisticsReportPDFView - '
+            'report %s downloaded: StudentStatisticsReport_rno%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
+
+    def test_report_purge(self):
+        self.trigger_report_creation()
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Purge").click()
+        self.assertTrue('0 report(s) purged' in self.browser.contents)
+        job_id, gen_name, user = self.app['reports'].get_running_report_jobs(user_id=None)[0]
+        job = getUtility(IJobManager).get(job_id)
+        tz = getUtility(IKofaUtils).tzinfo
+        delta = timedelta(weeks=5)
+        setattr(job, '_begin_after', datetime.now(tz) - delta)
+        self.browser.getControl("Purge").click()
+        self.assertTrue('1 report(s) purged' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s purged' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/student.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/student.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/student.py	(revision 16045)
@@ -0,0 +1,645 @@
+## $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
+##
+"""
+Container for the various objects owned by students.
+"""
+import os
+import re
+import shutil
+import grok
+from datetime import datetime, timedelta
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from zope.password.interfaces import IPasswordManager
+from zope.component import getUtility, createObject
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.event import notify
+
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.image import KofaImageFile
+from waeup.kofa.imagestorage import DefaultFileStoreHandler
+from waeup.kofa.interfaces import (
+    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
+    IKofaUtils, registration_states_vocab, IExtFileStore,
+    CREATED, ADMITTED, CLEARANCE, PAID, REGISTERED, VALIDATED, RETURNING,
+    GRADUATED, TRANSVAL, TRANSREL)
+from waeup.kofa.students.accommodation import StudentAccommodation
+from waeup.kofa.students.interfaces import (
+    IStudent, IStudentNavigation, IStudentPersonalEdit, ICSVStudentExporter,
+    IStudentsUtils)
+from waeup.kofa.students.payments import StudentPaymentsContainer
+from waeup.kofa.students.utils import generate_student_id
+from waeup.kofa.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
+
+RE_STUDID_NON_NUM = re.compile('[^\d]+')
+
+class Student(grok.Container):
+    """This is a student container for the various objects
+    owned by students.
+    """
+    grok.implements(IStudent, IStudentNavigation, IStudentPersonalEdit)
+    grok.provides(IStudent)
+
+    def __init__(self):
+        super(Student, self).__init__()
+        # The site doesn't exist in unit tests
+        try:
+            self.student_id = generate_student_id()
+        except TypeError:
+            self.student_id = u'Z654321'
+        self.password = None
+        self.temp_password = None
+        self.parents_password = None
+        return
+
+    def setTempPassword(self, user, password):
+        """Set a temporary password (LDAP-compatible) SSHA encoded for
+        officers.
+        """
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.temp_password = {}
+        self.temp_password[
+            'password'] = passwordmanager.encodePassword(password)
+        self.temp_password['user'] = user
+        self.temp_password['timestamp'] = datetime.utcnow() # offset-naive datetime
+
+    def getTempPassword(self):
+        """Check if a temporary password has been set and if it
+        is not expired. 
+
+        Return the temporary password if valid,
+        None otherwise. Unset the temporary password if expired.
+        """
+        temp_password_dict = getattr(self, 'temp_password', None)
+        kofa_utils = getUtility(IKofaUtils)
+        temp_password_minutes =  kofa_utils.TEMP_PASSWORD_MINUTES
+        if temp_password_dict is not None:
+            delta = timedelta(minutes=temp_password_minutes)
+            now = datetime.utcnow()
+            if now < temp_password_dict.get('timestamp') + delta:
+                return temp_password_dict.get('password')
+            else:
+                # Unset temporary password if expired
+                self.temp_password = None
+        return None
+
+    def setParentsPassword(self, password):
+        """Set a temporary password (LDAP-compatible) SSHA encoded for
+        parents.
+        """
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        self.parents_password = {}
+        self.parents_password[
+            'password'] = passwordmanager.encodePassword(password)
+        self.parents_password['timestamp'] = datetime.utcnow() # offset-naive datetime
+
+    def getParentsPassword(self):
+        """Check if a parents password has been set and if it
+        is not expired.
+
+        Return the parents password if valid,
+        None otherwise. Unset the parents password and replace roles
+        if expired.
+        """
+        parents_password_dict = getattr(self, 'parents_password', None)
+        kofa_utils = getUtility(IKofaUtils)
+        temp_password_minutes =  kofa_utils.TEMP_PASSWORD_MINUTES
+        if parents_password_dict is not None:
+            delta = timedelta(minutes=temp_password_minutes)
+            now = datetime.utcnow()
+            if now < parents_password_dict.get('timestamp') + delta:
+                return parents_password_dict.get('password')
+            else:
+                # Unset parents password if expired
+                self.parents_password = None
+                # Replace roles if expired
+                role_manager = IPrincipalRoleManager(self)
+                role_manager.removeRoleFromPrincipal(
+                    'waeup.local.Parents', self.student_id)
+                notify(LocalRoleSetEvent(
+                    self, 'waeup.local.Parents',
+                    self.student_id, granted=False))
+                role_manager.assignRoleToPrincipal(
+                    'waeup.local.StudentRecordOwner', self.student_id)
+                notify(LocalRoleSetEvent(
+                    self, 'waeup.local.StudentRecordOwner',
+                    self.student_id, granted=True))
+        return None
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.logger.info(
+            '%s - %s - %s' % (ob_class, self.__name__, message))
+        return
+
+    @property
+    def display_fullname(self):
+        middlename = getattr(self, 'middlename', None)
+        kofa_utils = getUtility(IKofaUtils)
+        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
+
+    @property
+    def fullname(self):
+        middlename = getattr(self, 'middlename', None)
+        if middlename:
+            return '%s-%s-%s' % (self.firstname.lower(),
+                middlename.lower(), self.lastname.lower())
+        else:
+            return '%s-%s' % (self.firstname.lower(), self.lastname.lower())
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def translated_state(self):
+        try:
+            state = registration_states_vocab.getTermByToken(
+                self.state).title
+        except LookupError:  # in unit tests
+            return
+        return state
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def student(self):
+        return self
+
+    @property
+    def certcode(self):
+        cert = getattr(self.get('studycourse', None), 'certificate', None)
+        if cert is not None:
+            return cert.code
+        return
+
+    @property
+    def faccode(self):
+        cert = getattr(self.get('studycourse', None), 'certificate', None)
+        if cert is not None:
+            return cert.__parent__.__parent__.__parent__.code
+        return
+
+    @property
+    def depcode(self):
+        cert = getattr(self.get('studycourse', None), 'certificate', None)
+        if cert is not None:
+            return cert.__parent__.__parent__.code
+        return
+
+    @property
+    def current_session(self):
+        session = getattr(
+            self.get('studycourse', None), 'current_session', None)
+        return session
+
+    @property
+    def entry_session(self):
+        session = getattr(
+            self.get('studycourse', None), 'entry_session', None)
+        return session
+
+    @property
+    def entry_mode(self):
+        session = getattr(
+            self.get('studycourse', None), 'entry_mode', None)
+        return session
+
+    @property
+    def current_level(self):
+        level = getattr(
+            self.get('studycourse', None), 'current_level', None)
+        return level
+
+    @property
+    def current_verdict(self):
+        level = getattr(
+            self.get('studycourse', None), 'current_verdict', None)
+        return level
+
+    @property
+    def current_mode(self):
+        certificate = getattr(
+            self.get('studycourse', None), 'certificate', None)
+        if certificate is not None:
+            return certificate.study_mode
+        return None
+
+    @property
+    def is_postgrad(self):
+        is_postgrad = getattr(
+            self.get('studycourse', None), 'is_postgrad', False)
+        return is_postgrad
+
+    @property
+    def is_special_postgrad(self):
+        is_special_postgrad = getattr(
+            self.get('studycourse', None), 'is_special_postgrad', False)
+        return is_special_postgrad
+
+    @property
+    def is_fresh(self):
+        return self.current_session == self.entry_session
+
+    @property
+    def before_payment(self):
+        non_fresh_states = (PAID, REGISTERED, VALIDATED, RETURNING, GRADUATED)
+        if self.is_fresh and self.state not in non_fresh_states:
+            return True
+        return False
+
+    @property
+    def personal_data_expired(self):
+        if self.state in (CREATED, ADMITTED,):
+            return False
+        now = datetime.utcnow()
+        if self.personal_updated is None:
+            return True
+        days_ago = getattr(now - self.personal_updated, 'days')
+        if days_ago > 180:
+            return True
+        return False
+
+    @property
+    def transcript_enabled(self):
+        return True
+
+    @property
+    def studycourse_locked(self):
+        return self.state in (GRADUATED, TRANSREL, TRANSVAL)
+
+    @property
+    def clearance_locked(self):
+        return self.state != CLEARANCE
+
+    def transfer(self, certificate, current_session=None,
+        current_level=None, current_verdict=None, previous_verdict=None):
+        """ Creates a new studycourse and backups the old one.
+        """
+        newcourse = createObject(u'waeup.StudentStudyCourse')
+        try:
+            newcourse.certificate = certificate
+            newcourse.entry_mode = 'transfer'
+            newcourse.current_session = current_session
+            newcourse.current_level = current_level
+            newcourse.current_verdict = current_verdict
+            newcourse.previous_verdict = previous_verdict
+        except ConstraintNotSatisfied:
+            return -1
+        oldcourse = self['studycourse']
+        if getattr(oldcourse, 'entry_session', None) is None or\
+            getattr(oldcourse, 'certificate', None) is None:
+            return -2
+        newcourse.entry_session = oldcourse.entry_session
+        # Students can be transferred only two times.
+        if 'studycourse_1' in self.keys():
+            if 'studycourse_2' in self.keys():
+                return -3
+            self['studycourse_2'] = oldcourse
+        else:
+            self['studycourse_1'] = oldcourse
+        del self['studycourse']
+        self['studycourse'] = newcourse
+        self.__parent__.logger.info(
+            '%s - transferred from %s to %s' % (
+            self.student_id,
+            oldcourse.certificate.code,
+            newcourse.certificate.code))
+        history = IObjectHistory(self)
+        history.addMessage('Transferred from %s to %s' % (
+            oldcourse.certificate.code, newcourse.certificate.code))
+        return
+
+    def revert_transfer(self):
+        """ Revert previous transfer.
+
+        """
+        if not self.has_key('studycourse_1'):
+            return -1
+        del self['studycourse']
+        if 'studycourse_2' in self.keys():
+            studycourse = self['studycourse_2']
+            self['studycourse'] = studycourse
+            del self['studycourse_2']
+        else:
+            studycourse = self['studycourse_1']
+            self['studycourse'] = studycourse
+            del self['studycourse_1']
+        self.__parent__.logger.info(
+            '%s - transfer reverted' % self.student_id)
+        history = IObjectHistory(self)
+        history.addMessage('Transfer reverted')
+        return
+
+# Set all attributes of Student required in IStudent as field
+# properties. Doing this, we do not have to set initial attributes
+# ourselves and as a bonus we get free validation when an attribute is
+# set.
+Student = attrs_to_fields(Student)
+
+class StudentFactory(grok.GlobalUtility):
+    """A factory for students.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Student')
+    title = u"Create a new student.",
+    description = u"This factory instantiates new student instances."
+
+    def __call__(self, *args, **kw):
+        return Student()
+
+    def getInterfaces(self):
+        return implementedBy(Student)
+
+@grok.subscribe(IStudent, grok.IObjectAddedEvent)
+def handle_student_added(student, event):
+    """If a student is added all subcontainers are automatically added
+    and the transition create is fired. The latter produces a logging
+    message.
+    """
+    studycourse = createObject(u'waeup.StudentStudyCourse')
+    student['studycourse'] = studycourse
+    payments = StudentPaymentsContainer()
+    student['payments'] = payments
+    accommodation = StudentAccommodation()
+    student['accommodation'] = accommodation
+    # Assign global student role for new student
+    account = IUserAccount(student)
+    account.roles = ['waeup.Student']
+    # Assign local StudentRecordOwner role
+    role_manager = IPrincipalRoleManager(student)
+    role_manager.assignRoleToPrincipal(
+        'waeup.local.StudentRecordOwner', student.student_id)
+    if student.state is None:
+        IWorkflowInfo(student).fireTransition('create')
+    return
+
+def path_from_studid(student_id):
+    """Convert a student_id into a predictable relative folder path.
+
+    Used for storing files.
+
+    Returns the name of folder in which files for a particular student
+    should be stored. This is a relative path, relative to any general
+    students folder with 5 zero-padded digits (except when student_id
+    is overlong).
+
+    We normally map 1,000 different student ids into one single
+    path. For instance ``K1000000`` will give ``01000/K1000000``,
+    ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will
+    result in ``1234/K12345678``.
+
+    For lower numbers < 10**6 we return the same path for up to 10,000
+    student_ids. So for instance ``KM123456`` will result in
+    ``00120/KM123456`` (there will be no path starting with
+    ``00123``).
+
+    Works also with overlong number: here the leading zeros will be
+    missing but ``K123456789`` will give reliably
+    ``12345/K123456789`` as expected.
+    """
+    # remove all non numeric characters and turn this into an int.
+    num = int(RE_STUDID_NON_NUM.sub('', student_id))
+    if num < 10**6:
+        # store max. of 10000 studs per folder and correct num for 5 digits
+        num = num / 10000 * 10
+    else:
+        # store max. of 1000 studs per folder
+        num = num / 1000
+    # format folder name to have 5 zero-padded digits
+    folder_name = u'%05d' % num
+    folder_name = os.path.join(folder_name, student_id)
+    return folder_name
+
+def move_student_files(student, del_dir):
+    """Move files belonging to `student` to `del_dir`.
+
+    `del_dir` is expected to be the path to the site-wide directory
+    for storing backup data.
+
+    The current files of the student are removed after backup.
+
+    If the student has no associated files stored, nothing is done.
+    """
+    stud_id = student.student_id
+
+    src = getUtility(IExtFileStore).root
+    src = os.path.join(src, 'students', path_from_studid(stud_id))
+
+    dst = os.path.join(
+        del_dir, 'media', 'students', path_from_studid(stud_id))
+
+    if not os.path.isdir(src):
+        # Do not copy if no files were stored.
+        return
+    if not os.path.exists(dst):
+        os.makedirs(dst, 0755)
+    copy_filesystem_tree(src, dst)
+    shutil.rmtree(src)
+    return
+
+def update_student_deletion_csvs(student, del_dir):
+    """Update deletion CSV files with data from student.
+
+    `del_dir` is expected to be the path to the site-wide directory
+    for storing backup data.
+
+    Each exporter available for students (and their many subobjects)
+    is called in order to export CSV data of the given student to csv
+    files in the site-wide backup directory for object data (see
+    DataCenter).
+
+    Each exported row is appended a column giving the deletion date
+    (column `del_date`) as a UTC timestamp.
+    """
+
+    STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
+        IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
+
+    for name in STUDENT_BACKUP_EXPORTER_NAMES:
+        exporter = getUtility(ICSVStudentExporter, name=name)
+        csv_data = exporter.export_student(student)
+        csv_data = csv_data.split('\r\n')
+
+        # append a deletion timestamp on each data row
+        timestamp = str(now().replace(microsecond=0)) # store UTC timestamp
+        for num, row in enumerate(csv_data[1:-1]):
+            csv_data[num+1] = csv_data[num+1] + ',' + timestamp
+        csv_path = os.path.join(del_dir, '%s.csv' % name)
+
+        # write data to CSV file
+        if not os.path.exists(csv_path):
+            # create new CSV file (including header line)
+            csv_data[0] = csv_data[0] + ',del_date'
+            open(csv_path, 'wb').write('\r\n'.join(csv_data))
+        else:
+            # append existing CSV file (omitting headerline)
+            open(csv_path, 'a').write('\r\n'.join(csv_data[1:]))
+    return
+
+@grok.subscribe(IStudent, grok.IObjectRemovedEvent)
+def handle_student_removed(student, event):
+    """If a student is removed a message is logged and data is put
+       into a backup location.
+
+    The data of the removed student is appended to CSV files in local
+    datacenter and any existing external files (passport images, etc.)
+    are copied over to this location as well.
+
+    Documents in the file storage refering to the given student are
+    removed afterwards (if they exist). Please make no assumptions
+    about how the deletion takes place. Files might be deleted
+    individually (leaving the students file directory intact) or the
+    whole student directory might be deleted completely.
+
+    All CSV rows created/appended contain a timestamp with the
+    datetime of removal in an additional `del_date` column.
+
+    XXX: blocking of used student_ids yet not implemented.
+    """
+    comment = 'Student record removed'
+    target = student.student_id
+    try:
+        site = grok.getSite()
+        site['students'].logger.info('%s - %s' % (
+            target, comment))
+    except KeyError:
+        # If we delete an entire university instance there won't be
+        # a students subcontainer
+        return
+
+    del_dir = site['datacenter'].deleted_path
+
+    if student.state == GRADUATED:
+        del_dir = site['datacenter'].graduated_path
+
+    # save files of the student
+    move_student_files(student, del_dir)
+
+    # update CSV files
+    update_student_deletion_csvs(student, del_dir)
+
+    # remove global role
+    role_manager = IPrincipalRoleManager(grok.getSite())
+    role_manager.unsetRoleForPrincipal('waeup.Student', student.student_id)
+    return
+
+#: The file id marker for student files
+STUDENT_FILE_STORE_NAME = 'file-student'
+
+class StudentFileNameChooser(grok.Adapter):
+    """A file id chooser for :class:`Student` objects.
+
+    `context` is an :class:`Student` instance.
+
+    The :class:`StudentImageNameChooser` can build/check file ids for
+    :class:`Student` objects suitable for use with
+    :class:`ExtFileStore` instances. The delivered file_id contains
+    the file id marker for :class:`Student` object and the student id
+    of the context student.
+
+    This chooser is registered as an adapter providing
+    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
+
+    File store name choosers like this one are only convenience
+    components to ease the task of creating file ids for student
+    objects. You are nevertheless encouraged to use them instead of
+    manually setting up filenames for students.
+
+    .. seealso:: :mod:`waeup.kofa.imagestorage`
+
+    """
+    grok.context(IStudent)
+    grok.implements(IFileStoreNameChooser)
+
+    def checkName(self, name=None, attr=None):
+        """Check whether the given name is a valid file id for the context.
+
+        Returns ``True`` only if `name` equals the result of
+        :meth:`chooseName`.
+
+        """
+        return name == self.chooseName()
+
+    def chooseName(self, attr, name=None):
+        """Get a valid file id for student context.
+
+        *Example:*
+
+        For a student with student id ``'A123456'`` and
+        with attr ``'nice_image.jpeg'`` stored in
+        the students container this chooser would create:
+
+          ``'__file-student__students/A/A123456/nice_image_A123456.jpeg'``
+
+        meaning that the nice image of this applicant would be
+        stored in the site-wide file storage in path:
+
+          ``students/A/A123456/nice_image_A123456.jpeg``
+
+        """
+        basename, ext = os.path.splitext(attr)
+        stud_id = self.context.student_id
+        marked_filename = '__%s__%s/%s_%s%s' % (
+            STUDENT_FILE_STORE_NAME, path_from_studid(stud_id), basename,
+            stud_id, ext)
+        return marked_filename
+
+
+class StudentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
+    """Student specific file handling.
+
+    This handler knows in which path in a filestore to store student
+    files and how to turn this kind of data into some (browsable)
+    file object.
+
+    It is called from the global file storage, when it wants to
+    get/store a file with a file id starting with
+    ``__file-student__`` (the marker string for student files).
+
+    Like each other file store handler it does not handle the files
+    really (this is done by the global file store) but only computes
+    paths and things like this.
+    """
+    grok.implements(IFileStoreHandler)
+    grok.name(STUDENT_FILE_STORE_NAME)
+
+    def pathFromFileID(self, store, root, file_id):
+        """All student files are put in directory ``students``.
+        """
+        marker, filename, basename, ext = store.extractMarker(file_id)
+        sub_root = os.path.join(root, 'students')
+        return super(StudentFileStoreHandler, self).pathFromFileID(
+            store, sub_root, basename)
+
+    def createFile(self, store, root, filename, file_id, file):
+        """Create a browsable file-like object.
+        """
+        # call super method to ensure that any old files with
+        # different filename extension are deleted.
+        file, path, file_obj =  super(
+            StudentFileStoreHandler, self).createFile(
+            store, root,  filename, file_id, file)
+        return file, path, KofaImageFile(
+            file_obj.filename, file_obj.data)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studycourse.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studycourse.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studycourse.py	(revision 16045)
@@ -0,0 +1,179 @@
+## $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
+##
+"""
+Container which holds the data of the student study courses
+and contains the (student) study level objects.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.component import createObject
+from zope.interface import implementedBy
+from waeup.kofa.students.interfaces import (
+    IStudentStudyCourse, IStudentNavigation, IStudentStudyLevel)
+from waeup.kofa.students.studylevel import CourseTicket
+from waeup.kofa.students.workflow import CLEARED, RETURNING, PAID
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+class StudentStudyCourse(grok.Container):
+    """This is a container for study levels.
+    """
+    grok.implements(IStudentStudyCourse, IStudentNavigation)
+    grok.provides(IStudentStudyCourse)
+
+    is_special_postgrad = False
+
+    def __init__(self):
+        super(StudentStudyCourse, self).__init__()
+        return
+
+    @property
+    def student(self):
+        return self.__parent__
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.writeLogMessage(view, message)
+
+    @property
+    def next_session_allowed(self):
+        certificate = getattr(self, 'certificate', None)
+        if certificate == None:
+            return False
+        if self.student.state in (CLEARED, RETURNING):
+            return True
+        if self.student.state == PAID \
+            and self.student.is_postgrad:
+            return True
+        return False
+
+    @property
+    def is_postgrad(self):
+        if self.certificate is None:
+            return False
+        return self.certificate.study_mode.startswith('pg')
+
+    @property
+    def is_current(self):
+        if self.__name__ and '_' in self.__name__:
+            return False
+        return True
+
+    @property
+    def is_previous(self):
+        if self.__name__ == 'studycourse_2':
+            return True
+        if self.__name__ == 'studycourse_1' and  \
+            not self.__parent__.has_key('studycourse_2'):
+            return True
+        return False
+
+    def addStudentStudyLevel(self, cert, studylevel):
+        """Add a study level object.
+        """
+        if not IStudentStudyLevel.providedBy(studylevel):
+            raise TypeError(
+                'StudentStudyCourses contain only IStudentStudyLevel instances')
+        self[str(studylevel.level)] = studylevel
+        studylevel.addCertCourseTickets(cert)
+        # Collect carry-over courses in base levels (not in repeating levels)
+        try:
+            co_enabled = grok.getSite()['configuration'].carry_over
+        except TypeError:
+            # In tests we might not have a site object
+            co_enabled = True
+        if not co_enabled or studylevel.level % 100 != 0:
+            return
+        levels = sorted(self.keys())
+        index = levels.index(str(studylevel.level))
+        if index <= 0:
+            return
+        previous_level = self[levels[index-1]]
+        for key, val in previous_level.items():
+            if val.total_score >= val.passmark:
+                continue
+            if key in self[str(studylevel.level)]:
+                # Carry-over ticket exists
+                continue
+            co_ticket = createObject(u'waeup.CourseTicket')
+            for name in ['code', 'title', 'credits', 'passmark',
+                         'semester', 'mandatory', 'fcode', 'dcode']:
+                setattr(co_ticket, name, getattr(val, name))
+            co_ticket.automatic = True
+            co_ticket.carry_over = True
+            self[str(studylevel.level)][co_ticket.code] = co_ticket
+        return
+
+    def getTranscriptData(self):
+        """Get a sorted list of dicts with level and course ticket data.
+
+        This method is used for transcripts.
+        """
+        levels = []
+        cgpa = 0.0
+        total_credits_counted = 0
+        for level_key, level_val in self.items():
+            # Skip Level Zero
+            if level_key == '0':
+                continue
+            tickets_1 = []
+            tickets_2 = []
+            tickets_3 = []
+            tickets = sorted(level_val.values(), key=lambda ticket: ticket.code)
+            for ticket in tickets:
+                if ticket.outstanding:
+                    continue
+                if ticket.semester == 1:
+                    tickets_1.append(ticket)
+                elif ticket.semester == 2:
+                    tickets_2.append(ticket)
+                elif ticket.semester == 3:
+                    tickets_3.append(ticket)
+            gpa_params = level_val.gpa_params_rectified
+            sgpa = gpa_params[0]
+            total_credits_counted += gpa_params[1]
+            cgpa += gpa_params[2]
+            levels.append(
+                dict(level_key=level_key,
+                     level=level_val,
+                     tickets_1=tickets_1,
+                     tickets_2=tickets_2,
+                     tickets_3=tickets_3,
+                     sgpa=sgpa))
+        if total_credits_counted:
+            cgpa = cgpa/total_credits_counted
+        # Override cgpa if value has been imported
+        # (not implemented in base package)
+        imported_cgpa = getattr(self, 'imported_cgpa', None)
+        if imported_cgpa:
+            cgpa = imported_cgpa
+        return sorted(levels, key=lambda level: level['level_key']), cgpa
+
+StudentStudyCourse = attrs_to_fields(StudentStudyCourse)
+
+class StudentStudyCourseFactory(grok.GlobalUtility):
+    """A factory for student study courses.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.StudentStudyCourse')
+    title = u"Create a new student study course.",
+    description = u"This factory instantiates new student study course instances."
+
+    def __call__(self, *args, **kw):
+        return StudentStudyCourse()
+
+    def getInterfaces(self):
+        return implementedBy(StudentStudyCourse)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studylevel.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studylevel.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/studylevel.py	(revision 16045)
@@ -0,0 +1,456 @@
+## $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
+##
+"""
+Container which holds the data of a student study level
+and contains the course tickets.
+"""
+import grok
+import pytz
+from datetime import datetime
+from zope.component.interfaces import IFactory
+from zope.catalog.interfaces import ICatalog
+from zope.component import createObject, queryUtility, getUtility
+from zope.interface import implementedBy
+from zope.event import notify
+from waeup.kofa.interfaces import academic_sessions_vocab, VALIDATED, IKofaUtils
+from waeup.kofa.students.interfaces import (
+    IStudentStudyLevel, IStudentNavigation, ICourseTicket)
+from waeup.kofa.utils.helpers import attrs_to_fields
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.interfaces import MessageFactory as _
+
+@grok.subscribe(IStudentStudyLevel, grok.IObjectModifiedEvent)
+def handle_update_coursetickets(studylevel, event):
+    """If level_session has changed, coursetickets_catalog
+    must be informed.
+    """
+    # Catalog must be informed
+    for ticket in studylevel.values():
+        notify(grok.ObjectModifiedEvent(ticket))
+    return
+
+def find_carry_over(ticket):
+    studylevel = ticket.__parent__
+    studycourse = ticket.__parent__.__parent__
+    levels = sorted(studycourse.keys())
+    index = levels.index(str(studylevel.level))
+    try:
+        next_level = levels[index+1]
+    except IndexError:
+        return None
+    next_studylevel = studycourse[next_level]
+    co_ticket = next_studylevel.get(ticket.code, None)
+    return co_ticket
+
+class StudentStudyLevel(grok.Container):
+    """This is a container for course tickets.
+    """
+    grok.implements(IStudentStudyLevel, IStudentNavigation)
+    grok.provides(IStudentStudyLevel)
+
+    def __init__(self):
+        super(StudentStudyLevel, self).__init__()
+        return
+
+    @property
+    def student(self):
+        try:
+            return self.__parent__.__parent__
+        except AttributeError:
+            return None
+
+    @property
+    def certcode(self):
+        try:
+            return self.__parent__.certificate.code
+        except AttributeError:
+            return None
+
+    @property
+    def number_of_tickets(self):
+        return len(self)
+
+    @property
+    def total_credits(self):
+        total = 0
+        for ticket in self.values():
+            if not ticket.outstanding:
+                total += ticket.credits
+        return total
+
+    @property
+    def getSessionString(self):
+        try:
+            session_string = academic_sessions_vocab.getTerm(
+                self.level_session).title
+        except LookupError:
+            return None
+        return session_string
+
+    @property
+    def gpa_params_rectified(self):
+        """Calculate corrected level (sessional) gpa parameters.
+        The corrected gpa is displayed on transcripts only.
+        """
+        credits_weighted = 0.0
+        credits_counted = 0
+        level_gpa = 0.0
+        for ticket in self.values():
+            if ticket.carry_over is False and ticket.total_score:
+                if ticket.total_score < ticket.passmark:
+                    co_ticket = find_carry_over(ticket)
+                    if co_ticket is not None and co_ticket.weight is not None:
+                        credits_counted += co_ticket.credits
+                        credits_weighted += co_ticket.credits * co_ticket.weight
+                    continue
+                credits_counted += ticket.credits
+                credits_weighted += ticket.credits * ticket.weight
+        if credits_counted:
+            level_gpa = credits_weighted/credits_counted
+        return level_gpa, credits_counted, credits_weighted
+
+    @property
+    def gpa_params(self):
+        """Calculate gpa parameters for this level.
+        """
+        credits_weighted = 0.0
+        credits_counted = 0
+        level_gpa = 0.0
+        for ticket in self.values():
+            if ticket.total_score is not None:
+                credits_counted += ticket.credits
+                credits_weighted += ticket.credits * ticket.weight
+        if credits_counted:
+            level_gpa = credits_weighted / credits_counted
+        # Override level_gpa if value has been imported
+        # (not implemented in base package)
+        imported_gpa = getattr(self, 'imported_gpa', None)
+        if imported_gpa:
+            level_gpa = imported_gpa
+        return level_gpa, credits_counted, credits_weighted
+
+    @property
+    def gpa(self):
+        """Return string formatted gpa value.
+        """
+        format_float = getUtility(IKofaUtils).format_float
+        return format_float(self.gpa_params[0], 2)
+
+    @property
+    def passed_params(self):
+        """Determine the number and credits of passed and failed courses.
+        Count the number of courses registered but not taken.
+        This method is used for level reports and the
+        OutstandingCoursesExporter.
+        """
+        passed = failed = 0
+        courses_failed = ''
+        credits_failed = 0
+        credits_passed = 0
+        courses_not_taken = ''
+        for ticket in self.values():
+            if ticket.total_score is not None:
+                if ticket.total_score < ticket.passmark:
+                    failed += 1
+                    credits_failed += ticket.credits
+                    if ticket.mandatory:
+                        courses_failed += 'm_%s_m ' % ticket.code
+                    else:
+                        courses_failed += '%s ' % ticket.code
+                else:
+                    passed += 1
+                    credits_passed += ticket.credits
+            else:
+                courses_not_taken += '%s ' % ticket.code
+        return (passed, failed, credits_passed,
+                credits_failed, courses_failed,
+                courses_not_taken)
+
+    @property
+    def cumulative_params(self):
+        """Calculate the cumulative gpa and other cumulative parameters
+        for this level.
+        All levels below this level are taken under consideration
+        (including repeating levels).
+        This method is used for level reports and meanwhile also 
+        for session results presentations.
+        """
+        credits_passed = 0
+        total_credits = 0
+        total_credits_counted = 0
+        total_credits_weighted = 0
+        cgpa = 0.0
+        if self.__parent__:
+            for level in self.__parent__.values():
+                if level.level > self.level:
+                    continue
+                credits_passed += level.passed_params[2]
+                total_credits += level.total_credits
+                gpa_params = level.gpa_params
+                total_credits_counted += gpa_params[1]
+                total_credits_weighted += gpa_params[2]
+            if total_credits_counted:
+                cgpa = total_credits_weighted/total_credits_counted
+            # Override cgpa if value has been imported
+            # (not implemented in base package)
+            imported_cgpa = getattr(self, 'imported_cgpa', None)
+            if imported_cgpa:
+                cgpa = imported_cgpa
+        return (cgpa, total_credits_counted, total_credits_weighted,
+               total_credits, credits_passed)
+
+    @property
+    def is_current_level(self):
+        try:
+            return self.__parent__.current_level == self.level
+        except AttributeError:
+            return False
+
+    def writeLogMessage(self, view, message):
+        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+        self.__parent__.__parent__.__parent__.logger.info(
+            '%s - %s - level %s - %s' % (
+                ob_class,
+                self.__parent__.__parent__.__name__,
+                self.__name__,
+                message))
+        return
+
+    @property
+    def level_title(self):
+        studylevelsource = StudyLevelSource()
+        return studylevelsource.factory.getTitle(self.__parent__, self.level)
+
+    @property
+    def course_registration_forbidden(self):
+        try:
+            deadline = grok.getSite()['configuration'][
+                str(self.level_session)].coursereg_deadline
+        except (TypeError, KeyError):
+            return 
+        if not deadline or deadline > datetime.now(pytz.utc):
+            return 
+        if len(self.student['payments']):
+            for ticket in self.student['payments'].values():
+                if ticket.p_category == 'late_registration' and \
+                    ticket.p_session == self.level_session and \
+                    ticket.p_state == 'paid':
+                        return
+        return _("Course registration has ended. "
+                 "Please pay the late registration fee.")
+
+    def addCourseTicket(self, ticket, course):
+        """Add a course ticket object.
+        """
+        if not ICourseTicket.providedBy(ticket):
+            raise TypeError(
+                'StudentStudyLevels contain only ICourseTicket instances')
+        ticket.code = course.code
+        ticket.title = course.title
+        ticket.fcode = course.__parent__.__parent__.__parent__.code
+        ticket.dcode = course.__parent__.__parent__.code
+        ticket.credits = course.credits
+        ticket.passmark = course.passmark
+        ticket.semester = course.semester
+        self[ticket.code] = ticket
+        return
+
+    def addCertCourseTickets(self, cert):
+        """Collect all certificate courses and create course
+        tickets automatically.
+        """
+        if cert is not None:
+            for key, val in cert.items():
+                if val.level != self.level:
+                    continue
+                ticket = createObject(u'waeup.CourseTicket')
+                ticket.automatic = True
+                ticket.mandatory = val.mandatory
+                ticket.carry_over = False
+                ticket.course_category = val.course_category
+                self.addCourseTicket(ticket, val.course)
+        return
+
+    def updateCourseTicket(self, ticket, course):
+        """Updates a course ticket object and return code
+        if ticket has been invalidated.
+        """
+        if not course:
+            if ticket.title.endswith('cancelled)'):
+                # Skip this tiket
+                return
+            # Invalidate course ticket
+            ticket.title += u' (course cancelled)'
+            ticket.credits = 0
+            ticket.passmark = 0
+            return ticket.code
+        ticket.code = course.code
+        ticket.title = course.title
+        ticket.fcode = course.__parent__.__parent__.__parent__.code
+        ticket.dcode = course.__parent__.__parent__.code
+        ticket.credits = course.credits
+        ticket.passmark = course.passmark
+        ticket.semester = course.semester
+        return
+
+StudentStudyLevel = attrs_to_fields(
+    StudentStudyLevel, omit=['total_credits', 'gpa'])
+
+class StudentStudyLevelFactory(grok.GlobalUtility):
+    """A factory for student study levels.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.StudentStudyLevel')
+    title = u"Create a new student study level.",
+    description = u"This factory instantiates new student study level instances."
+
+    def __call__(self, *args, **kw):
+        return StudentStudyLevel()
+
+    def getInterfaces(self):
+        return implementedBy(StudentStudyLevel)
+
+class CourseTicket(grok.Model):
+    """This is a course ticket which allows the
+    student to attend the course. Lecturers will enter scores and more at
+    the end of the term.
+
+    A course ticket contains a copy of the original course and
+    certificate course data. If the courses and/or the referring certificate
+    courses are removed, the corresponding tickets remain unchanged.
+    So we do not need any event triggered actions on course tickets.
+    """
+    grok.implements(ICourseTicket, IStudentNavigation)
+    grok.provides(ICourseTicket)
+
+    def __init__(self):
+        super(CourseTicket, self).__init__()
+        self.code = None
+        return
+
+    @property
+    def student(self):
+        """Get the associated student object.
+        """
+        try:
+            return self.__parent__.__parent__.__parent__
+        except AttributeError: # in unit tests
+            return None
+
+    @property
+    def certcode(self):
+        try:
+            return self.__parent__.__parent__.certificate.code
+        except AttributeError: # in unit tests
+            return None
+
+    @property
+    def removable_by_student(self):
+        """True if student is allowed to remove the ticket.
+        """
+        return not self.mandatory
+
+    @property
+    def editable_by_lecturer(self):
+        """True if lecturer is allowed to edit the ticket.
+        """
+        try:
+            cas = grok.getSite()[
+                'configuration'].current_academic_session
+            if self.student.state == VALIDATED and \
+                self.student.current_session == cas:
+                return True
+        except (AttributeError, TypeError): # in unit tests
+            pass
+        return False
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.__parent__.writeLogMessage(
+            view, message)
+
+    @property
+    def level(self):
+        """Returns the id of the level the ticket has been added to.
+        """
+        try:
+            return self.__parent__.level
+        except AttributeError: # in unit tests
+            return None
+
+    @property
+    def level_session(self):
+        """Returns the session of the level the ticket has been added to.
+        """
+        try:
+            return self.__parent__.level_session
+        except AttributeError: # in unit tests
+            return None
+
+    @property
+    def total_score(self):
+        """Returns the total score of this ticket. In the base package
+        this is simply the score. In customized packages this could be
+        something else.
+        """
+        return self.score
+
+    @property
+    def _getGradeWeightFromScore(self):
+        """Nigerian Course Grading System
+        """
+        if self.total_score is None:
+            return (None, None)
+        if self.total_score >= 70:
+            return ('A',5)
+        if self.total_score >= 60:
+            return ('B',4)
+        if self.total_score >= 50:
+            return ('C',3)
+        if self.total_score >= 45:
+            return ('D',2)
+        if self.total_score >= self.passmark: # passmark changed in 2013 from 40 to 45
+            return ('E',1)
+        return ('F',0)
+
+    @property
+    def grade(self):
+        """Returns the grade calculated from total score.
+        """
+        return self._getGradeWeightFromScore[0]
+
+    @property
+    def weight(self):
+        """Returns the weight calculated from total score.
+        """
+        return self._getGradeWeightFromScore[1]
+
+CourseTicket = attrs_to_fields(CourseTicket)
+
+class CourseTicketFactory(grok.GlobalUtility):
+    """A factory for student study levels.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CourseTicket')
+    title = u"Create a new course ticket.",
+    description = u"This factory instantiates new course ticket instances."
+
+    def __call__(self, *args, **kw):
+        return CourseTicket()
+
+    def getInterfaces(self):
+        return implementedBy(CourseTicket)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_courseticket_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 16045)
@@ -0,0 +1,10 @@
+reg_number,matric_number,level,code,score,mandatory,level_session,ticket_session,unlock_score
+1,,100,COURSE1,1,True,,,
+2,,100,COURSE1,2,False,,,
+,100002,100,COURSE1,3,False,,,
+1,,200,COURSE1,1,True,2008,,
+1,,nonsense,COURSE1,5,,,,
+1,,100,NONSENSE,5,,,,
+1,,200,COURSE1,6,,2004,,
+1,,300,COURSE1,6,,2008,,
+1,,300,COURSE1,6,,2008X,,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_create_payment_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 16045)
@@ -0,0 +1,15 @@
+p_state,p_category,reg_number,p_session,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type
+paid,schoolfee,1,2003,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2004,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2005,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2006,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2007,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2008,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2009,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2011,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2012,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2013,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2014,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2015,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2015,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_payment_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 16045)
@@ -0,0 +1,6 @@
+matric_number,p_state,p_category,p_id,reg_number,p_session,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type,p_current
+,paid,schoolfee,3816951290797973744#,1,2010,19500,BTECHBDT,19500,0942,00,2010/11/26 19:59:33.744 GMT+1,online,1
+100001,unpaid,schoolfee,3816951290712593757,,2010,0,BTECHBDT,19500.1,0942,Z0,2010-11-25 20:16:33.757 WAT,online,0
+,paid,schoolfee,p1266236341955,3,2009,19500,BTECHBDT,19500,0615,00,2010/02/15 13:19:01#,online,1
+,paid,schoolfee,ABC-11-1234567890,3,2011,19500,BTECHBDT,19500.6,,SC,2010/02/15 13:19:01#,sc,1
+,paid,schoolfee,<IGNORE>,3,2012,20500,BTECHBDT,20500,0615,00,2020/02/15 13:19:01#,online,1
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data.csv	(revision 16045)
@@ -0,0 +1,13 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,state,password
+X666666,Aaren,Pieri,1,1990-01-02,100000,,1234,M,courses validated,test1234
+Y777777,Claus,Finau,2,1990-01-03,100001,aa@aa.ng,1234,m,courses validated,
+,Susann,Berson,3,1990-01-04,100002,aa@aa.ng,1234,F,courses validated,
+,Else,Mueller,4,1990-01-05,100003,aa@aa.ng,1234,f,school fee paid,
+,,,,,,,,,,
+,,,,  ,,, ,,,
+,,,,<IGNORE>,,,<IGNORE>,,,
+X888888,Alfons,Meier,5,1990-01-02,100004,aa@aa.ng,1234,m,courses validated,
+X999999,Herbert,Mueller,6,1990-01-02,100005,aa@aa.ng,1234,m,courses registered,
+X111111,Fabian,Riester,7,1990-01-02,100006,aa@aa.ng,1234,m,courses validated,
+X222222,Thomas,Kinderman,8,1990-01-02,100007,aa@aa.ng,1234,m,courses validated,
+X333333,Paula,Schall,9,1990-01-02,100008,aa@aa.ng,1234,m,courses registered,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv	(revision 16045)
@@ -0,0 +1,6 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,password,sex,state
+A123456,Aaren,Pieri,1,1990-01-02#,100000,aa@aa.ng,1234,mypw1,m,clearance started
+B123456,Aaren,Finau,1,1990-01-03,100001,aa@aa.ng,1234,mypw1,m,cleared
+C123456,Aaren,Berson,2,1990-01-04,100000,aa@aa.ng,1234,mypw1,m,admitted
+F123456,Frank,Meyer,1,1990-01-06,100000,aa@aa.ng,1234,,m,
+A123456,Uli,Schulz,3,1990-01-07,100002,aa@aa.ng,1234,,m,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_extascii.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_extascii.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_extascii.csv	(revision 16045)
@@ -0,0 +1,3 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,state
+X111111,Fabian,Müßig,1,1990-01-02,100006,aa@aa.ng,1234,m,courses validated
+X222222,Thomas,Kinderman,8äöü,1990-01-02,100007,aa@aa.ng,1234,m,courses validated
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_migration.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_migration.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_migration.csv	(revision 16045)
@@ -0,0 +1,7 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,password,sex,state
+A123456,Aaren,Pieri,1,1990-01-02#,100000,aa@aa.ng,1234,mypw1,m,clearance started
+B123456,Aaren,Finau,2,1990-01-03,100001,aa@aa.ng,1234,mypw1,m,cleared
+C123456,Aaren,Berson,3,1990-01-04,100002,aa@aa.ng,1234,{SSHA}JJBeA47pgGah500NFpuM2tVeLAbM8dxj,m,admitted
+D123456,John,Wolter,4,1990-01-05,100003,aa@aa.ng,1234,mypw1,m,nonsense
+E123456,John,Kennedy,5,1990-01-06,100004,aa@aa.ng,1234,,x,
+F123456,Frank,Meyer,6,1990-01-06,100005,aa@aa.ng,1234,,m,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update.csv	(revision 16045)
@@ -0,0 +1,4 @@
+reg_number,date_of_birth,matric_number,state
+1,1980-01-02,10,admitted
+2,1980-01-03,11,
+3,1980-01-04,12,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update2.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update2.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update2.csv	(revision 16045)
@@ -0,0 +1,4 @@
+date_of_birth,matric_number,phone,password
+1970-01-02,100000,,XXX
+1970-01-03,100001,XXX,
+1970-01-04,100002,<IGNORE>,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update3.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update3.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update3.csv	(revision 16045)
@@ -0,0 +1,6 @@
+student_id,reg_number,transition,firstname
+X666666,,request_clearance,
+Y777777,,return,
+,3,return,
+,4,register_courses,
+X666666,,,XXX
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update4.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update4.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_student_data_update4.csv	(revision 16045)
@@ -0,0 +1,2 @@
+student_id,reg_number,transition,state
+X666666,,request_clearance,returning
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studycourse_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studycourse_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studycourse_data.csv	(revision 16045)
@@ -0,0 +1,11 @@
+reg_number,matric_number,certificate,current_session,current_level
+1,,CERT1,2008,200
+2,,CERT1,2008,200
+,100002,CERT1,2008,200
+1,,CERT1,2008,100
+4,,CERT1,2008,200
+5,,CERT1,2008,200
+6,,CERT1,2008,200
+7,,CERT1,2008,200
+8,,CERT1,2008,200
+9,,CERT1,2008,200
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studylevel_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studylevel_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_studylevel_data.csv	(revision 16045)
@@ -0,0 +1,13 @@
+reg_number,matric_number,level_session,level_verdict,level
+1,,2008,,100
+1,,2008,,200
+2,,2008,B,100
+,100002,2009,C,100
+1,,2008,A,111
+1,,2008,A,nonsense
+1,,2008,A,<IGNORE>
+4,,2008,A,200
+5,,2008,A,200
+6,,2008,A,200
+7,,2008,A,200
+9,,2008,A,200
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_transfer_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_transfer_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_transfer_data.csv	(revision 16045)
@@ -0,0 +1,2 @@
+reg_number,matric_number,certificate,current_session,current_level,entry_mode
+123,,CERT2,2009,300,transfer
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_verdict_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_verdict_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/sample_verdict_data.csv	(revision 16045)
@@ -0,0 +1,10 @@
+matric_number,current_session,current_level,current_verdict,bypass_validation,validated_by
+100000,2008,200,0,0,
+100001,2008,100,B,False,
+100002,2007,200,C,,
+100003,2008,200,A,,
+100004,2008,200,,,
+100005,2008,200,A,1,
+100006,2008,200,A,True,
+100007,2008,200,A,True,
+100008,2008,200,A,True,Juliana
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_authentication.py	(revision 16045)
@@ -0,0 +1,253 @@
+## $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
+##
+import unittest
+from datetime import datetime, timedelta
+from zope.authentication.interfaces import IAuthentication
+from zope.component import provideUtility, queryUtility, getGlobalSiteManager
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.password import SSHAPasswordManager
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth import PluggableAuthentication
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.role import Role
+from zope.securitypolicy.interfaces import IRole, Allow
+from waeup.kofa.authentication import get_principal_role_manager
+from waeup.kofa.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.kofa.students.authentication import (
+    StudentsAuthenticatorSetup, StudentAccount)
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+
+class StudentsAuthenticatorSetupTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = StudentsAuthenticatorSetup()
+        verifyClass(IAuthPluginUtility, StudentsAuthenticatorSetup)
+        verifyObject(IAuthPluginUtility, obj)
+        return
+
+    def test_register(self):
+        # Make sure registration works.
+        setup = StudentsAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        setup.register(pau)
+        self.assertTrue('students' in pau.authenticatorPlugins)
+        return
+
+    def test_unregister(self):
+        # Make sure deregistration works.
+        setup = StudentsAuthenticatorSetup()
+        pau = PluggableAuthentication()
+        pau.authenticatorPlugins = ('students')
+        setup.unregister(pau)
+        self.assertTrue('students' not in pau.authenticatorPlugins)
+        return
+
+
+class FakeStudent(object):
+    student_id = 'test_stud'
+    display_fullname = 'Test User'
+    password = None
+    email = None
+    phone = None
+    suspended = False
+    temp_password_minutes = 10
+
+    def setTempPassword(self, user, password):
+        passwordmanager = queryUtility(IPasswordManager, 'SSHA')
+        self.temp_password = {}
+        self.temp_password[
+            'password'] = passwordmanager.encodePassword(password)
+        self.temp_password['user'] = user
+        self.temp_password['timestamp'] = datetime.utcnow()
+
+    def getTempPassword(self):
+        temp_password_dict = getattr(self, 'temp_password', None)
+        if temp_password_dict is not None:
+            delta = timedelta(minutes=self.temp_password_minutes)
+            now = datetime.utcnow()
+            if now < temp_password_dict.get('timestamp') + delta:
+                return temp_password_dict.get('password')
+            else:
+                # Unset temporary password if expired
+                self.temp_password = None
+        return None
+
+    def getParentsPassword(self):
+        parents_password_dict = getattr(self, 'parents_password', None)
+        if parents_password_dict is not None:
+            delta = timedelta(minutes=self.temp_password_minutes)
+            now = datetime.utcnow()
+            if now < parents_password_dict.get('timestamp') + delta:
+                return parents_password_dict.get('password')
+            else:
+                # Unset temporary password if expired
+                self.parents_password = None
+        return None
+
+
+
+class MinimalPAU(PluggableAuthentication):
+    def getPrincipal(self, id):
+        return 'faked principal'
+
+class StudentAccountTests(unittest.TestCase):
+
+    def setUp(self):
+        self.fake_stud = FakeStudent()
+        self.account = StudentAccount(self.fake_stud)
+
+        # We provide a minimal PAU
+        pau = MinimalPAU()
+        provideUtility(pau, IAuthentication)
+
+        # We register a role
+        test_role = Role('waeup.test.Role', 'Testing Role')
+        provideUtility(test_role, IRole, name='waeup.test.Role')
+
+        # We have to setup a password manager utility manually as we
+        # have no functional test. In functional tests this would
+        # happen automatically, but it would take a lot more time to
+        # run the tests.
+        provideUtility(
+            SSHAPasswordManager(), IPasswordManager, 'SSHA')
+        return
+
+    def tearDown(self):
+        self.account.roles = [] # make sure roles are reset
+        gsm = getGlobalSiteManager()
+        to_clean = []
+        # Clear up utilities registered in setUp
+        to_clean.append(
+            (IPasswordManager, queryUtility(
+                    IPasswordManager, name='SSHA', default=None)))
+        to_clean.append(
+            (IAuthentication, queryUtility(IAuthentication, default=None)))
+        to_clean.append(
+            (IRole, queryUtility(IRole, name='test.Role', default=None)))
+        for iface, elem in to_clean:
+            if elem is not None:
+                gsm.unregisterUtility(elem, iface)
+        return
+
+    def test_iface(self):
+        verifyClass(IUserAccount, StudentAccount)
+        verifyObject(IUserAccount, self.account)
+        return
+
+    def test_set_password(self):
+        # make sure we can set a password.
+        self.account.setPassword('secret')
+        self.assertTrue(self.fake_stud.password is not None)
+        # we do not store plaintext passwords
+        self.assertTrue(self.fake_stud.password != 'secret')
+        # passwords are stored as bytestreams
+        self.assertTrue(isinstance(self.fake_stud.password, basestring))
+        self.assertFalse(isinstance(self.fake_stud.password, unicode))
+        return
+
+    def test_check_password(self):
+        # make sure we can check a password.
+        self.account.setPassword('secret')
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword('nonsense')
+        result3 = self.account.checkPassword('secret')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        self.assertEqual(result3, True)
+        return
+
+    def test_check_temp_password(self):
+        # make sure that, if a temp password is set,
+        # this password is used for authentication
+        self.account.setPassword('secret')
+        self.fake_stud.setTempPassword(user='beate', password='temp_secret')
+        result1 = self.account.checkPassword('secret')
+        result2 = self.account.checkPassword(None)
+        result3 = self.account.checkPassword('nonsense')
+        result4 = self.account.checkPassword('temp_secret')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        self.assertEqual(result3, False)
+        self.assertEqual(result4, True)
+        # if the temp password is expired, the original password
+        # is used again
+        delta = timedelta(minutes=11)
+        self.fake_stud.temp_password['timestamp'] = datetime.utcnow() - delta
+        result5 = self.account.checkPassword('temp_secret')
+        result6 = self.account.checkPassword('secret')
+        self.assertEqual(result5, False)
+        self.assertEqual(result6, True)
+        return
+
+    def test_check_unset_password(self):
+        # empty and unset passwords do not match anything
+        self.fake_stud.password = None
+        result1 = self.account.checkPassword('')
+        self.fake_stud.password = ''
+        result2 = self.account.checkPassword('')
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_check_password_no_string(self):
+        # if passed in password is not a string, we gain no access
+        self.fake_stud.password = 'secret'
+        result1 = self.account.checkPassword(None)
+        result2 = self.account.checkPassword(object())
+        self.assertEqual(result1, False)
+        self.assertEqual(result2, False)
+        return
+
+    def test_role_set(self):
+        # make sure we can set roles for principals denoted by account
+        prm = get_principal_role_manager()
+        self.assertEqual(prm.getPrincipalsAndRoles(), [])
+        self.account.roles = ['waeup.test.Role']
+        self.assertEqual(
+            prm.getPrincipalsAndRoles(),
+            [('waeup.test.Role', 'test_stud', Allow)])
+        return
+
+    def test_role_get(self):
+        # make sure we can get roles set for an account
+        self.assertEqual(self.account.roles, [])
+        self.account.roles = ['waeup.test.Role',] # set a role
+        self.assertEqual(self.account.roles, ['waeup.test.Role'])
+        return
+
+
+
+class FunctionalStudentAuthTests(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalStudentAuthTests, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalStudentAuthTests, self).tearDown()
+        return
+
+    def test_reset_protected_anonymous(self):
+        # anonymous users cannot reset others passwords
+        self.assertRaises(
+            Unauthorized,
+            self.browser.open, self.student_path + '/change_password')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,1443 @@
+# -*- coding: utf-8 -*-
+## $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
+##
+"""Unit tests for students-related data processors.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+import grok
+from time import time
+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.interface.verify import verifyClass, verifyObject
+from hurry.workflow.interfaces import IWorkflowState
+
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import (
+    IBatchProcessor, FatalCSVError, IUserAccount, DuplicationError)
+from waeup.kofa.students.batching import (
+    StudentProcessor, StudentStudyCourseProcessor,
+    StudentStudyLevelProcessor, CourseTicketProcessor,
+    StudentOnlinePaymentProcessor, StudentVerdictProcessor)
+from waeup.kofa.students.payments import StudentOnlinePayment
+from waeup.kofa.students.student import Student
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.kofa.students.accommodation import BedTicket
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
+
+
+STUDENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS = STUDENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data_update.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_UPDATE = STUDENT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_UPDATE2 = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data_update2.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_UPDATE2 = STUDENT_SAMPLE_DATA_UPDATE2.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_UPDATE3 = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data_update3.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_UPDATE3 = STUDENT_SAMPLE_DATA_UPDATE3.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_UPDATE4 = open(
+    os.path.join(os.path.dirname(__file__), 'sample_student_data_update4.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_UPDATE4 = STUDENT_SAMPLE_DATA_UPDATE4.split(
+    '\n')[0].split(',')
+
+STUDYCOURSE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_studycourse_data.csv'),
+    'rb').read()
+
+STUDYCOURSE_HEADER_FIELDS = STUDYCOURSE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+TRANSFER_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_transfer_data.csv'),
+    'rb').read()
+
+TRANSFER_HEADER_FIELDS = TRANSFER_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+VERDICT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_verdict_data.csv'),
+    'rb').read()
+
+VERDICT_HEADER_FIELDS = VERDICT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_MIGRATION = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_student_data_migration.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_MIGRATION = STUDENT_SAMPLE_DATA_MIGRATION.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_DUPLICATES = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_student_data_duplicates.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_DUPLICATES = STUDENT_SAMPLE_DATA_DUPLICATES.split(
+    '\n')[0].split(',')
+
+STUDENT_SAMPLE_DATA_EXTASCII = open(
+    os.path.join(os.path.dirname(__file__),
+                 'sample_student_data_extascii.csv'),
+    'rb').read()
+
+STUDENT_HEADER_FIELDS_EXTASCII = STUDENT_SAMPLE_DATA_EXTASCII.split(
+    '\n')[0].split(',')
+
+STUDYLEVEL_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_studylevel_data.csv'),
+    'rb').read()
+
+STUDYLEVEL_HEADER_FIELDS = STUDYLEVEL_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+COURSETICKET_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_courseticket_data.csv'),
+    'rb').read()
+
+COURSETICKET_HEADER_FIELDS = COURSETICKET_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+PAYMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_payment_data.csv'),
+    'rb').read()
+
+PAYMENT_HEADER_FIELDS = PAYMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+PAYMENT_CREATE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_create_payment_data.csv'),
+    'rb').read()
+
+PAYMENT_CREATE_HEADER_FIELDS = PAYMENT_CREATE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+curr_year = datetime.datetime.now().year
+
+class StudentImportExportSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentImportExportSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.start_level = 200
+        self.certificate.end_level = 500
+        self.certificate.study_mode = u'ug_ft'
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+
+        # Create a hostel with two beds
+        hostel = Hostel()
+        hostel.hostel_id = u'hall-1'
+        hostel.hostel_name = u'Hall 1'
+        self.app['hostels'].addHostel(hostel)
+        bed = Bed()
+        bed.bed_id = u'hall-1_A_101_A'
+        bed.bed_number = 1
+        bed.owner = NOT_OCCUPIED
+        bed.bed_type = u'regular_male_fr'
+        self.app['hostels'][hostel.hostel_id].addBed(bed)
+        bed = Bed()
+        bed.bed_id = u'hall-1_A_101_B'
+        bed.bed_number = 2
+        bed.owner = NOT_OCCUPIED
+        bed.bed_type = u'regular_female_fr'
+        self.app['hostels'][hostel.hostel_id].addBed(bed)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        return
+
+    def tearDown(self):
+        super(StudentImportExportSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def setup_for_export(self):
+        student = Student()
+        student.student_id = u'A111111'
+        self.app['students'][student.student_id] = self.student = student
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def setup_student(self, student):
+        # set predictable values for `student`
+        student.matric_number = u'234'
+        student.adm_code = u'my adm code'
+        student.clr_code = u'my clr code'
+        student.perm_address = u'Studentroad 21\nLagos 123456\n'
+        student.reg_number = u'123'
+        student.firstname = u'Anna'
+        student.lastname = u'Tester'
+        student.middlename = u'M.'
+        student.date_of_birth = datetime.date(1981, 2, 4)
+        student.sex = 'f'
+        student.email = 'anna@sample.com'
+        student.phone = u'+234-123-12345'
+        student.notice = u'Some notice\nin lines.'
+        student.nationality = u'NG'
+
+        student['studycourse'].certificate = self.certificate
+        student['studycourse'].entry_mode = 'ug_ft'
+        student['studycourse'].entry_session = 2010
+        student['studycourse'].current_session = 2012
+        student['studycourse'].current_level = int(self.certificate.start_level)
+
+        study_level = StudentStudyLevel()
+        study_level.level_session = 2012
+        study_level.level_verdict = "A"
+        study_level.level = 100
+        student['studycourse'].addStudentStudyLevel(
+            self.certificate, study_level)
+
+        ticket = CourseTicket()
+        ticket.automatic = True
+        ticket.carry_over = True
+        ticket.code = u'CRS1'
+        ticket.title = u'Course 1'
+        ticket.fcode = u'FAC1'
+        ticket.dcode = u'DEP1'
+        ticket.credits = 100
+        ticket.passmark = 100
+        ticket.semester = 2
+        study_level[ticket.code] = ticket
+
+        bedticket = BedTicket()
+        bedticket.booking_session = 2004
+        bedticket.bed_type = u'any bed type'
+        bedticket.bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        student['accommodation'].addBedTicket(bedticket)
+
+        self.add_payment(student)
+        return student
+
+    def add_payment(self, student):
+        # get a payment with all fields set
+        payment = StudentOnlinePayment()
+        payment.creation_date = datetime.datetime(curr_year-6, 4, 1, 13, 12, 1)
+        payment.p_id = 'my-id'
+        payment.p_category = u'schoolfee'
+        payment.p_state = 'paid'
+        payment.ac = u'666'
+        payment.p_item = u'p-item'
+        payment.p_level = 100
+        payment.p_session = curr_year - 6
+        payment.payment_date = datetime.datetime(curr_year-6, 4, 1, 14, 12, 1)
+        payment.amount_auth = 12.12
+        payment.r_amount_approved = 12.12
+        payment.r_code = u'r-code'
+        # XXX: there is no addPayment method to give predictable names
+        self.payment = student['payments']['my-payment'] = payment
+        return payment
+
+
+class StudentProcessorTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentProcessorTest, self).setUp()
+
+        # Add student with subobjects
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+
+        self.processor = StudentProcessor()
+        self.csv_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        self.csv_file_update = os.path.join(
+            self.workdir, 'sample_student_data_update.csv')
+        self.csv_file_update2 = os.path.join(
+            self.workdir, 'sample_student_data_update2.csv')
+        self.csv_file_update3 = os.path.join(
+            self.workdir, 'sample_student_data_update3.csv')
+        self.csv_file_update4 = os.path.join(
+            self.workdir, 'sample_student_data_update4.csv')
+        self.csv_file_migration = os.path.join(
+            self.workdir, 'sample_student_data_migration.csv')
+        self.csv_file_duplicates = os.path.join(
+            self.workdir, 'sample_student_data_duplicates.csv')
+        self.csv_file_extascii = os.path.join(
+            self.workdir, 'sample_student_data_extascii.csv')
+        open(self.csv_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        open(self.csv_file_update, 'wb').write(STUDENT_SAMPLE_DATA_UPDATE)
+        open(self.csv_file_update2, 'wb').write(STUDENT_SAMPLE_DATA_UPDATE2)
+        open(self.csv_file_update3, 'wb').write(STUDENT_SAMPLE_DATA_UPDATE3)
+        open(self.csv_file_update4, 'wb').write(STUDENT_SAMPLE_DATA_UPDATE4)
+        open(self.csv_file_migration, 'wb').write(STUDENT_SAMPLE_DATA_MIGRATION)
+        open(self.csv_file_duplicates, 'wb').write(STUDENT_SAMPLE_DATA_DUPLICATES)
+        open(self.csv_file_extascii, 'wb').write(STUDENT_SAMPLE_DATA_EXTASCII)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, StudentProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.processor.parentsExist(None, dict()))
+        self.assertTrue(self.processor.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(student_id='ID_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(reg_number='123'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.processor.getParent(None, self.app)
+        assert parent is self.app['students']
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(student_id='ID_NONE'), self.app) is None
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id), self.app) is self.student
+
+    def test_addEntry(self):
+        new_student = Student()
+        self.processor.addEntry(
+            new_student, dict(), self.app)
+        assert len(self.app['students'].keys()) == 2
+
+    def test_checkConversion(self):
+        # Make sure we can check conversions and that the stud_id
+        # counter is not raised during such checks.
+        initial_stud_id = self.app['students']._curr_stud_id
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state='admitted'))
+        self.assertEqual(len(errs),0)
+        # Empty state is allowed
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state=''))
+        self.assertEqual(len(errs),0)
+        #self.assertTrue(('state', 'no value provided') in errs)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', state='nonsense'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('state', 'not allowed') in errs)
+        new_stud_id = self.app['students']._curr_stud_id
+        self.assertEqual(initial_stud_id, new_stud_id)
+        return
+
+    def test_checkUpdateRequirements(self):
+        # Make sure that pg students can't be updated with wrong transition.
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', state='returning'), self.app)
+        self.assertTrue(err is None)
+        self.certificate.study_mode = 'pg_ft'
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', state='returning'), self.app)
+        self.assertEqual(err, 'State not allowed (pg student).')
+        IWorkflowState(self.student).setState('school fee paid')
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', transition='reset6'), self.app)
+        self.assertEqual(err, 'Transition not allowed (pg student).')
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', transition='register_courses'), self.app)
+        self.assertEqual(err, 'Transition not allowed (pg student).')
+
+
+    def test_delEntry(self):
+        assert self.student.student_id in self.app['students'].keys()
+        self.processor.delEntry(
+            dict(reg_number=self.student.reg_number), self.app)
+        assert self.student.student_id not in self.app['students'].keys()
+
+    def test_import(self):
+        self.assertEqual(self.app['students']._curr_stud_id, 1000001)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        self.assertEqual(num_warns, 0)
+        # Nine students have been added.
+        self.assertEqual(len(self.app['students']), 10)
+        # Three empty rows have been skipped.
+        self.assertEqual(num, 12)
+        self.assertEqual(self.app['students']['X666666'].reg_number,'1')
+        self.assertEqual(
+            self.app['students']['X666666'].state, 'courses validated')
+        # Two new student_ids have been created.
+        self.assertEqual(self.app['students']._curr_stud_id, 1000003)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_extascii(self):
+        self.assertEqual(self.app['students']._curr_stud_id, 1000001)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_extascii, STUDENT_HEADER_FIELDS_EXTASCII)
+        self.assertEqual(num_warns,0)
+        self.assertEqual(len(self.app['students']), 3)
+        self.assertEqual(self.app['students']['X111111'].reg_number,'1')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, STUDENT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        # state has changed
+        self.assertEqual(self.app['students']['X666666'].state,'admitted')
+        # state has not changed
+        self.assertEqual(self.app['students']['Y777777'].state,
+                         'courses validated')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update2(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        container = self.app['students']
+        self.assertEqual(
+            IUserAccount(container['X666666']).checkPassword('test1234'), True)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update2, STUDENT_HEADER_FIELDS_UPDATE2, 'update')
+        self.assertEqual(num_warns,0)
+        # The phone import value of Pieri was None.
+        # Confirm that phone has not been cleared.
+        for key in container.keys():
+            if container[key].firstname == 'Aaren':
+                aaren = container[key]
+                break
+        self.assertEqual(aaren.phone, '--1234')
+        # The phone import value of Claus was a deletion marker.
+        # Confirm that phone has been cleared.
+        for key in container.keys():
+            if container[key].firstname == 'Claus':
+                claus = container[key]
+                break
+        assert claus.phone is None
+        # The password of X666666 has been removed
+        self.assertEqual(
+            IUserAccount(container['X666666']).password, None)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update3(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update3, STUDENT_HEADER_FIELDS_UPDATE3, 'update')
+        content = open(fail_file).read()
+        self.assertEqual(
+            content,
+            'reg_number,student_id,transition,firstname,--ERRORS--\r\n'
+            '<IGNORE>,X666666,request_clearance,<IGNORE>,Transition not allowed.\r\n'
+            '<IGNORE>,X666666,<IGNORE>,XXX,RequiredMissing: firstname\r\n'
+            )
+        self.assertEqual(num_warns,2)
+        self.assertEqual(self.app['students']['Y777777'].state,'returning')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update4(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        self.assertRaises(
+            FatalCSVError, self.processor.doImport, self.csv_file_update4,
+            STUDENT_HEADER_FIELDS_UPDATE4, 'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_update, STUDENT_HEADER_FIELDS_UPDATE, 'remove')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_migration_data(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_migration, STUDENT_HEADER_FIELDS_MIGRATION)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,2)
+        assert len(self.app['students'].keys()) == 5
+        self.assertEqual(
+            content,
+            'reg_number,firstname,student_id,sex,email,phone,state,date_of_birth,lastname,password,matric_number,--ERRORS--\r\n'
+            '4,John,D123456,m,aa@aa.ng,1234,nonsense,1990-01-05,Wolter,mypw1,100003,state: not allowed\r\n'
+            '5,John,E123456,x,aa@aa.ng,1234,<IGNORE>,1990-01-06,Kennedy,<IGNORE>,100004,sex: Invalid value\r\n'
+            )
+        students = self.app['students']
+        self.assertTrue('A123456' in students.keys())
+        self.assertEqual(students['A123456'].state, 'clearance started')
+        self.assertEqual(students['A123456'].date_of_birth,
+                         datetime.date(1990, 1, 2))
+        self.assertFalse(students['A123456'].clearance_locked)
+        self.assertEqual(students['B123456'].state, 'cleared')
+        self.assertEqual(students['B123456'].date_of_birth,
+                         datetime.date(1990, 1, 3))
+        self.assertTrue(students['B123456'].clearance_locked)
+        history = ' '.join(students['A123456'].history.messages)
+        self.assertTrue(
+            "State 'clearance started' set by system" in history)
+        # state was empty and student is thus in state created
+        self.assertEqual(students['F123456'].state,'created')
+        # passwords were set correctly
+        self.assertEqual(
+            IUserAccount(students['A123456']).checkPassword('mypw1'), True)
+        self.assertEqual(
+            IUserAccount(students['C123456']).checkPassword('mypw1'), True)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_duplicate_data(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_duplicates, STUDENT_HEADER_FIELDS_DUPLICATES)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,4)
+        self.assertEqual(
+            content,
+            'reg_number,firstname,student_id,sex,email,phone,state,date_of_birth,lastname,password,matric_number,--ERRORS--\r\n'
+            '1,Aaren,B123456,m,aa@aa.ng,1234,cleared,1990-01-03,Finau,mypw1,100001,reg_number: Invalid input\r\n'
+            '2,Aaren,C123456,m,aa@aa.ng,1234,admitted,1990-01-04,Berson,mypw1,100000,matric_number: Invalid input\r\n'
+            '1,Frank,F123456,m,aa@aa.ng,1234,<IGNORE>,1990-01-06,Meyer,<IGNORE>,100000,reg_number: Invalid input; matric_number: Invalid input\r\n'
+            '3,Uli,A123456,m,aa@aa.ng,1234,<IGNORE>,1990-01-07,Schulz,<IGNORE>,100002,This object already exists.\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class StudentStudyCourseProcessorTest(StudentImportExportSetup):
+
+    def setUp(self):
+        super(StudentStudyCourseProcessorTest, self).setUp()
+
+        # Add student with subobjects
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = StudentStudyCourseProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(self.csv_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        self.csv_file_transfer = os.path.join(
+            self.workdir, 'sample_transfer_data.csv')
+        open(self.csv_file_transfer, 'wb').write(TRANSFER_SAMPLE_DATA)
+        return
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, StudentStudyCourseProcessor) is True
+
+    def test_entryExists(self):
+        assert self.processor.entryExists(
+            dict(reg_number='REG_NONE'), self.app) is False
+        assert self.processor.entryExists(
+            dict(reg_number='1'), self.app) is True
+
+    def test_getEntry(self):
+        student = self.processor.getEntry(
+            dict(reg_number='1'), self.app).__parent__
+        self.assertEqual(student.reg_number,'1')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', certificate='CERT1', current_level='200'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', certificate='CERT999'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('certificate', u'Invalid value') in errs)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', certificate='CERT1', current_level='100'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('current_level','not in range') in errs)
+        # If we import only current_level, no conversion checking is done.
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', current_level='100'))
+        self.assertEqual(len(errs),0)
+
+    def test_checkUpdateRequirements(self):
+        # Current level must be in range of certificate.
+        # Since row has passed the converter, current_level is an integer.
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=100), self.app)
+        self.assertEqual(err, 'current_level not in range.')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=200), self.app)
+        self.assertTrue(err is None)
+        # We can update pg students.
+        self.student['studycourse'].certificate.start_level=999
+        self.student['studycourse'].certificate.end_level=999
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertTrue(err is None)
+        # Make sure that pg students can't be updated with wrong transition.
+        IWorkflowState(self.student).setState('returning')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertEqual(err, 'Not a pg student.')
+        # If certificate is not given in row (and has thus
+        # successfully passed checkConversion) the certificate
+        # attribute must be set.
+        self.student['studycourse'].certificate = None
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=100), self.app)
+        self.assertEqual(err, 'No certificate to check level.')
+        # When transferring students the method also checks
+        # if the former studycourse is complete.
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', certificate='CERT1', current_level=200,
+            entry_mode='transfer'), self.app)
+        self.assertEqual(err, 'Former study course record incomplete.')
+        self.student['studycourse'].certificate = self.certificate
+        self.student['studycourse'].entry_session = 2005
+        # The method doesn't care if current_level
+        # is not in range of CERT1. This is done by checkConversion
+        # if certificate is in row.
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', certificate='CERT1', current_level=200,
+            entry_mode='transfer'), self.app)
+        self.assertTrue(err is None)
+        # In state 'transcript validated' studycourses are locked.
+        IWorkflowState(self.student).setState('transcript validated')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertEqual(err, 'Studycourse is locked.')
+        # In state 'graduated' studycourses are also locked.
+        IWorkflowState(self.student).setState('graduated')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertEqual(err, 'Studycourse is locked.')
+        # In state 'transcript released' studycourses are also locked ...
+        IWorkflowState(self.student).setState('transcript released')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertEqual(err, 'Studycourse is locked.')
+        # ... but not in state 'transcript requested'.
+        IWorkflowState(self.student).setState('transcript requested')
+        err = self.processor.checkUpdateRequirements(
+            self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertTrue(err is None)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,1)
+        content = open(fail_file).read()
+        self.assertTrue('current_level: not in range' in content)
+        studycourse = self.processor.getEntry(dict(reg_number='1'), self.app)
+        self.assertEqual(studycourse.certificate.code, u'CERT1')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_transfer(self):
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = 'CERT2'
+        self.certificate2.application_category = 'basic'
+        self.certificate2.start_level = 200
+        self.certificate2.end_level = 500
+        self.certificate2.study_mode = u'ug_pt'
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file_transfer, TRANSFER_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,0)
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
+        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
+        self.assertEqual(self.student['studycourse'].entry_mode, 'transfer')
+        self.assertEqual(self.student['studycourse_1'].entry_mode, 'ug_ft')
+        self.assertEqual(self.student.current_mode, 'ug_pt')
+        shutil.rmtree(os.path.dirname(fin_file))
+        # Transer has bee logged.
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000000 - transferred from CERT1 to CERT2\n'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - '
+            'StudentStudyCourse Processor (update only) - '
+            'sample_transfer_data - K1000000 - updated: entry_mode=transfer, '
+            'certificate=CERT2, current_session=2009, current_level=300'
+            in logcontent)
+        # A history message has been added.
+        history = ' '.join(self.student.history.messages)
+        self.assertTrue(
+            "Transferred from CERT1 to CERT2 by system" in history)
+        # The catalog has been updated
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(
+            certcode=('CERT2', 'CERT2')))
+        self.assertTrue(results[0] is self.student)
+        results = list(
+            cat.searchResults(
+            current_session=(2009, 2009)))
+        self.assertTrue(results[0] is self.student)
+        results = list(
+            cat.searchResults(
+            certcode=('CERT1', 'CERT1')))
+        self.assertEqual(len(results), 0)
+
+class StudentStudyLevelProcessorTest(StudentImportExportSetup):
+
+    def setUp(self):
+        super(StudentStudyLevelProcessorTest, self).setUp()
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Update study courses
+        studycourse_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(studycourse_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        processor = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = processor.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = StudentStudyLevelProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studylevel_data.csv')
+        open(self.csv_file, 'wb').write(STUDYLEVEL_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, StudentStudyLevelProcessor) is True
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', level='220'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', level='999'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', level='1000'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('level', u'Invalid value') in errs)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', level='xyz'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('level', u'Invalid value') in errs)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,3)
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100'), self.app) is True
+        studylevel = self.processor.getEntry(
+            dict(reg_number='1', level='100'), self.app)
+        self.assertEqual(studylevel.__parent__.certificate.code, u'CERT1')
+        self.assertEqual(studylevel.level_session, 2008)
+        self.assertEqual(studylevel.level_verdict, '0')
+        self.assertEqual(studylevel.level, 100)
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry,
+        self.assertTrue(
+            'INFO - system - StudentStudyLevel Processor - '
+            'sample_studylevel_data - K1000000 - updated: '
+            'level=100, level_verdict=C, level_session=2009'
+            in logcontent)
+        content = open(fail_file).read()
+        self.assertEqual(
+            content,
+            'reg_number,level_verdict,level_session,matric_number,level,'
+            '--ERRORS--\r\n'
+            '1,A,2008,<IGNORE>,111,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,nonsense,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,<IGNORE>,level: Invalid value\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        # We perform the same import twice,
+        # the second time in update mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,3)
+        studylevel = self.processor.getEntry(
+            dict(reg_number='1', level='100'), self.app)
+        self.assertEqual(studylevel.level, 100)
+        content = open(fail_file).read()
+        self.assertEqual(
+            content,
+            'reg_number,level_verdict,level_session,matric_number,level,'
+            '--ERRORS--\r\n'
+            '1,A,2008,<IGNORE>,111,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,nonsense,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,<IGNORE>,Cannot update: no such entry\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update_locked(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        # In state 'transcript validated' studylevels can't be edited
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        # Two more records could't be imported because course tickets
+        # of X666666 are locked
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,5)
+        content = open(fail_file).read()
+        self.assertEqual(
+            content,
+            'reg_number,level_verdict,level_session,matric_number,level,--ERRORS--\r\n'
+            '1,<IGNORE>,2008,<IGNORE>,100,Studylevel is locked.\r\n'
+            '1,<IGNORE>,2008,<IGNORE>,200,Studylevel is locked.\r\n'
+            '1,A,2008,<IGNORE>,111,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,nonsense,level: Invalid value\r\n'
+            '1,A,2008,<IGNORE>,<IGNORE>,Cannot update: no such entry\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'remove')
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100'), self.app) is False
+        self.assertEqual(num_warns,3)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class CourseTicketProcessorTest(StudentImportExportSetup):
+
+    def setUp(self):
+        super(CourseTicketProcessorTest, self).setUp()
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Add course and certificate course
+        self.course = createObject('waeup.Course')
+        self.course.code = 'COURSE1'
+        self.course.semester = 1
+        self.course.credits = 10
+        self.course.passmark = 40
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        self.app['faculties']['fac1']['dep1'].certificates[
+            'CERT1'].addCertCourse(
+            self.course, level=100)
+
+        # Update study courses
+        studycourse_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(studycourse_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        processor = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = processor.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Import study levels
+        processor = StudentStudyLevelProcessor()
+        studylevel_file = os.path.join(
+            self.workdir, 'sample_studylevel_data.csv')
+        open(studylevel_file, 'wb').write(STUDYLEVEL_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = processor.doImport(
+            studylevel_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = CourseTicketProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_courseticket_data.csv')
+        open(self.csv_file, 'wb').write(COURSETICKET_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, CourseTicketProcessor) is True
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', code='COURSE1', level='220'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', code='COURSE2', level='220'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('code','non-existent') in errs)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        fail_file = open(fail_file).read()
+        self.assertEqual(num_warns,5)
+        self.assertEqual(fail_file,
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Not all parents do exist yet.\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'),
+            self.app) is True
+        courseticket = self.processor.getEntry(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app)
+        self.assertEqual(courseticket.__parent__.__parent__.certificate.code,
+                         u'CERT1')
+        self.assertEqual(courseticket.score, 1)
+        self.assertEqual(courseticket.mandatory, True)
+        self.assertEqual(courseticket.fcode, 'NA')
+        self.assertEqual(courseticket.dcode, 'NA')
+        self.assertEqual(courseticket.code, 'COURSE1')
+        self.assertEqual(courseticket.title, 'Unnamed Course')
+        self.assertEqual(courseticket.credits, 10)
+        self.assertEqual(courseticket.passmark, 40)
+        self.assertEqual(courseticket.semester, 1)
+        self.assertEqual(courseticket.level, 100)
+        self.assertEqual(courseticket.level_session, 2008)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry,
+        self.assertTrue(
+            'INFO - system - CourseTicket Processor - '
+            'sample_courseticket_data - K1000000 - 100 - '
+            'updated: code=COURSE1, '
+            'mandatory=False, score=3'
+            in logcontent)
+
+        # The catalog has been updated
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        results = list(
+            cat.searchResults(
+            level=(100, 100)))
+        self.assertEqual(len(results),3)
+
+    def test_import_update(self):
+        # We perform the same import twice,
+        # the second time in update mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'update')
+        fail_file = open(fail_file).read()
+        self.assertEqual(num_warns,5)
+        self.assertEqual(fail_file,
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Cannot update: no such entry\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,5)
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is False
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000000 - Course ticket in 100 removed: COURSE1'
+            in logcontent)
+
+    def test_import_update_locked(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        # In state 'transcript validated' course tickets can't edited
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'update')
+        fail_file = open(fail_file).read()
+        # Two more records could't be imported because course tickets
+        # of X666666 are locked
+        self.assertEqual(num_warns,7)
+        self.assertEqual(fail_file,
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,True,100,<IGNORE>,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,True,200,2008,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Cannot update: no such entry\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_create_locked(self):
+        # In state 'transcript validated' course tickets can't be created
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        fail_file = open(fail_file).read()
+        self.assertEqual(num_warns,7)
+        self.assertEqual(fail_file,
+            'reg_number,code,mandatory,level,level_session,ticket_session,score,unlock_score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,True,100,<IGNORE>,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,True,200,2008,<IGNORE>,1,<IGNORE>,<IGNORE>,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,Not all parents do exist yet.\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,<IGNORE>,5,<IGNORE>,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,<IGNORE>,6,<IGNORE>,<IGNORE>,level object: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,<IGNORE>,6,<IGNORE>,<IGNORE>,level_session: Invalid value\r\n')
+        self.assertFalse(self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'),
+            self.app))
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove_locked(self):
+        # We perform the same import twice,
+        # the second time in remove mode. 
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        # In state 'transcript validated' course tickets can't be removed
+        student = self.app['students']['X666666']
+        IWorkflowState(student).setState('transcript validated')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,7)
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        logcontent = open(self.logfile).read()
+        self.assertFalse(
+            'INFO - system - X666666 - Course ticket in 100 removed: COURSE1'
+            in logcontent)
+        fail_file = open(fail_file).read()
+        self.assertEqual(fail_file,
+            'reg_number,code,matric_number,level,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,100,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,200,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,Cannot remove: no such entry\r\n'
+            '1,NONSENSE,<IGNORE>,100,Cannot remove: no such entry\r\n'
+            '1,COURSE1,<IGNORE>,200,Studycourse is locked.\r\n'
+            '1,COURSE1,<IGNORE>,300,Cannot remove: no such entry\r\n'
+            '1,COURSE1,<IGNORE>,300,Cannot remove: no such entry\r\n'
+            )
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class PaymentProcessorTest(StudentImportExportSetup):
+
+    def setUp(self):
+        super(PaymentProcessorTest, self).setUp()
+
+        # Add student with payment
+        student = Student()
+        student.firstname = u'Anna'
+        student.lastname = u'Tester'
+        student.reg_number = u'123'
+        student.matric_number = u'234'
+        self.app['students'].addStudent(student)
+        self.student = self.app['students'][student.student_id]
+        payment = createObject(u'waeup.StudentOnlinePayment')
+        payment.p_id = 'p120'
+        payment.p_session = 2012
+        payment.p_category = 'schoolfee'
+        payment.p_state = 'paid'
+        self.student['payments'][payment.p_id] = payment
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = StudentOnlinePaymentProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_payment_data.csv')
+        open(self.csv_file, 'wb').write(PAYMENT_SAMPLE_DATA)
+        self.csv_file2 = os.path.join(
+            self.workdir, 'sample_create_payment_data.csv')
+        open(self.csv_file2, 'wb').write(PAYMENT_CREATE_SAMPLE_DATA)
+
+    def test_interface(self):
+        # Make sure we fulfill the interface contracts.
+        assert verifyObject(IBatchProcessor, self.processor) is True
+        assert verifyClass(
+            IBatchProcessor, StudentOnlinePaymentProcessor) is True
+
+    def test_getEntry(self):
+        assert self.processor.getEntry(
+            dict(student_id='ID_NONE', p_id='nonsense'), self.app) is None
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is self.student['payments']['p120']
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='XXXXXX112'),
+            self.app) is self.student['payments']['p120']
+
+    def test_delEntry(self):
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is self.student['payments']['p120']
+        self.assertEqual(len(self.student['payments'].keys()),1)
+        self.processor.delEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app)
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is None
+        self.assertEqual(len(self.student['payments'].keys()),0)
+
+    def test_addEntry(self):
+        self.assertEqual(len(self.student['payments'].keys()),1)
+        payment1 = createObject(u'waeup.StudentOnlinePayment')
+        payment1.p_id = 'p234'
+        self.processor.addEntry(
+            payment1, dict(student_id=self.student.student_id, p_id='p234'),
+            self.app)
+        self.assertEqual(len(self.student['payments'].keys()),2)
+        self.assertEqual(self.student['payments']['p234'].p_id, 'p234')
+        payment2 = createObject(u'waeup.StudentOnlinePayment')
+        payment1.p_id = 'nonsense'
+        # payment1.p_id will be replaced if p_id doesn't start with 'p'
+        # and is not an old PIN
+        self.processor.addEntry(
+            payment2, dict(student_id=self.student.student_id, p_id='XXXXXX456'),
+            self.app)
+        self.assertEqual(len(self.student['payments'].keys()),3)
+        self.assertEqual(self.student['payments']['p560'].p_id, 'p560')
+        # Requirement added on 19/02/2015: same payment must not exist.
+        payment3 = createObject(u'waeup.StudentOnlinePayment')
+        payment3.p_id = 'p456'
+        payment3.p_session = 2012
+        payment3.p_category = 'schoolfee'
+        self.assertRaises(
+            DuplicationError, self.processor.addEntry, payment3,
+            dict(student_id=self.student.student_id, p_id='p456'), self.app)
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - StudentOnlinePayment Processor - K1000000 - '
+            'previous update cancelled'
+            in logcontent)
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='<IGNORE>'), mode='create')
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='<IGNORE>'), mode='update')
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'missing'))
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='3816951266236341955'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='p1266236341955'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='ABC-11-1234567890'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='nonsense'))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'invalid format'))
+        timestamp = ("%d" % int(time()*10000))[1:]
+        p_id = "p%s" % timestamp
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id=p_id))
+        self.assertEqual(len(errs),0)
+        dup_payment = createObject(u'waeup.StudentOnlinePayment')
+        dup_payment.p_id = 'XYZ-99-1234567890'
+        self.student['payments'][dup_payment.p_id] = dup_payment
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(p_id='XYZ-99-1234567890'), mode='create')
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs[0], ('p_id', u'p_id exists in K1000000 '))
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,0)
+
+        payment = self.processor.getEntry(dict(reg_number='1',
+            p_id='p2907979737440'), self.app)
+        self.assertEqual(payment.p_id, 'p2907979737440')
+        self.assertTrue(payment.p_current)
+        cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
+        self.assertEqual(cdate, "2010-11-26 18:59:33")
+        self.assertEqual(str(payment.creation_date.tzinfo),'UTC')
+
+        payment = self.processor.getEntry(dict(matric_number='100001',
+            p_id='p2907125937570'), self.app)
+        self.assertEqual(payment.p_id, 'p2907125937570')
+        self.assertEqual(payment.amount_auth, 19500.1)
+        self.assertFalse(payment.p_current)
+        cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
+        # Ooooh, still the old problem, see
+        # http://mail.dzug.org/mailman/archives/zope/2006-August/001153.html.
+        # WAT is interpreted as GMT-1 and not GMT+1
+        self.assertEqual(cdate, "2010-11-25 21:16:33")
+        self.assertEqual(str(payment.creation_date.tzinfo),'UTC')
+
+        payment = self.processor.getEntry(dict(reg_number='3',
+            p_id='ABC-11-1234567890'), self.app)
+        self.assertEqual(payment.amount_auth, 19500.6)
+
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - StudentOnlinePayment Processor - '
+            'sample_payment_data - K1000001 - updated: '
+            'p_item=BTECHBDT, creation_date=2010-02-15 13:19:01+00:00, '
+            'p_category=schoolfee, amount_auth=19500.0, p_current=True, '
+            'p_session=2009, '
+            'p_id=p1266236341955, r_code=00, r_amount_approved=19500.0, '
+            'p_state=paid'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - StudentOnlinePayment Processor - '
+            'sample_payment_data - K1000001 - updated: '
+            'p_item=BTECHBDT, creation_date=2010-02-15 13:19:01+00:00, '
+            'p_category=schoolfee, amount_auth=19500.6, p_current=True, '
+            'p_session=2011, '
+            'p_id=ABC-11-1234567890, r_code=SC, r_amount_approved=19500.0, '
+            'p_state=paid'
+            in logcontent)
+
+    def test_import_update(self):
+        # We perform the same import twice,
+        # the second time in update mode. The number
+        # of warnings increases becaus one p_id is missing.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,1)
+        content = open(fail_file).read()
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertTrue('p_id: missing' in content)
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings increases becaus one p_id is missing.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,1)
+        content = open(fail_file).read()
+        self.assertTrue('p_id: missing' in content)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000001 - Payment ticket removed: p1266236341955'
+            in logcontent)
+
+    def test_import_same_payment_exists(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file2, PAYMENT_CREATE_HEADER_FIELDS,'create')
+        # One payment with same session and category exists
+        self.assertEqual(num_warns,1)
+        content = open(fail_file).read()
+        self.assertTrue(
+            '1,942,online,BTECHBDT,2010/11/26 19:59:33.744 GMT+1,0,'
+            '19500,schoolfee,19500,2015,paid,'
+            'Same payment has already been made.'
+            in content)
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertEqual(len(self.app['students']['X666666']['payments']), 13)
+
+class StudentVerdictProcessorTest(StudentImportExportSetup):
+
+    def setUp(self):
+        super(StudentVerdictProcessorTest, self).setUp()
+
+        # Import students with subobjects
+        student_file = os.path.join(self.workdir, 'sample_student_data.csv')
+        open(student_file, 'wb').write(STUDENT_SAMPLE_DATA)
+        num, num_warns, fin_file, fail_file = StudentProcessor().doImport(
+            student_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Update study courses
+        studycourse_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(studycourse_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        processor = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = processor.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+        # Import study levels
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studylevel_data.csv')
+        open(self.csv_file, 'wb').write(STUDYLEVEL_SAMPLE_DATA)
+        processor = StudentStudyLevelProcessor()
+        num, num_warns, fin_file, fail_file = processor.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        content = open(fail_file).read()
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.processor = StudentVerdictProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_verdict_data.csv')
+        open(self.csv_file, 'wb').write(VERDICT_SAMPLE_DATA)
+        return
+
+    def test_import(self):
+        studycourse = self.processor.getEntry(dict(matric_number='100000'),
+                                              self.app)
+        self.assertEqual(studycourse['200'].level_verdict, '0')
+        student = self.processor.getParent(
+            dict(matric_number='100000'), self.app)
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, VERDICT_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,5)
+        self.assertEqual(studycourse.current_verdict, '0')
+        self.assertEqual(student.state, 'returning')
+        self.assertEqual(studycourse.current_level, 200)
+        self.assertEqual(studycourse['200'].level_verdict, '0')
+        student = self.processor.getParent(
+            dict(matric_number='100005'), self.app)
+        self.assertEqual(student.state, 'returning')
+        self.assertEqual(student['studycourse'].current_verdict, 'A')
+        self.assertEqual(studycourse.current_level, 200)
+        self.assertEqual(student['studycourse']['200'].validated_by, 'System')
+        self.assertTrue(isinstance(
+            student['studycourse']['200'].validation_date, datetime.datetime))
+        student = self.processor.getParent(
+            dict(matric_number='100008'), self.app)
+        self.assertEqual(student['studycourse']['200'].validated_by, 'Juliana')
+        content = open(fail_file).read()
+        self.assertEqual(
+            content,
+            'current_session,current_level,bypass_validation,current_verdict,'
+            'matric_number,validated_by,--ERRORS--\r\n'
+            '2008,100,False,B,100001,<IGNORE>,Current level does not correspond.\r\n'
+            '2007,200,<IGNORE>,C,100002,<IGNORE>,Current session does not correspond.\r\n'
+            '2008,200,<IGNORE>,A,100003,<IGNORE>,Student in wrong state.\r\n'
+            '2008,200,<IGNORE>,<IGNORE>,100004,<IGNORE>,No verdict in import file.\r\n'
+            '2008,200,True,A,100007,<IGNORE>,Study level object is missing.\r\n'
+            )
+        logcontent = open(self.logfile).read()
+        self.assertMatches(
+            '... INFO - system - Verdict Processor (special processor, '
+            'update only) - sample_verdict_data - X666666 - '
+            'updated: current_verdict=0...',
+            logcontent)
+        self.assertMatches(
+            '... INFO - system - X666666 - Returned...',
+            logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        StudentProcessorTest,StudentStudyCourseProcessorTest,
+        StudentStudyLevelProcessorTest,CourseTicketProcessorTest,
+        PaymentProcessorTest,StudentVerdictProcessorTest]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
+
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_browser.py	(revision 16045)
@@ -0,0 +1,4911 @@
+# -*- coding: utf-8 -*-
+## $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
+import base64
+from datetime import datetime, timedelta, date
+from StringIO import StringIO
+import os
+import grok
+from zc.async.testing import wait_for_result
+from zope.event import notify
+from zope.component import createObject, queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from zope.interface import implementedBy
+from zope.schema.fieldproperty import FieldProperty
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IFileStoreNameChooser, IExtFileStore
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.students.payments import StudentOnlinePayment
+from waeup.kofa.students.student import Student
+from waeup.kofa.students.studylevel import StudentStudyLevel
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.interfaces import IUserAccount, IJobManager, VALIDATED, CREATED
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+from waeup.kofa.tests.test_authentication import SECRET
+
+PH_LEN = 15911  # Length of placeholder file
+
+SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
+URL_LECTURER_LANDING = 'http://localhost/app/my_courses'
+
+curr_year = datetime.now().year
+
+def lookup_submit_value(name, value, browser):
+    """Find a button with a certain value."""
+    for num in range(0, 100):
+        try:
+            button = browser.getControl(name=name, index=num)
+            if button.value.endswith(value):
+                return button
+        except IndexError:
+            break
+    return None
+
+
+class StudentsFullSetup(FunctionalTestCase):
+    # A test case that only contains a setup and teardown
+    #
+    # Complete setup for students handlings is rather complex and
+    # requires lots of things created before we can start. This is a
+    # setup that does all this, creates a university, creates PINs,
+    # etc.  so that we do not have to bother with that in different
+    # test cases.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentsFullSetup, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        # we add the site immediately after creation to the
+        # ZODB. Catalogs and other local utilities are not setup
+        # before that step.
+        self.app = self.getRootFolder()['app']
+        # Set site here. Some of the following setup code might need
+        # to access grok.getSite() and should get our new app then
+        setSite(app)
+
+        # Add student with subobjects
+        student = createObject('waeup.Student')
+        student.firstname = u'Anna'
+        student.lastname = u'Tester'
+        student.reg_number = u'123'
+        student.matric_number = u'234'
+        student.sex = u'm'
+        student.email = 'aa@aa.ng'
+        student.phone = u'1234'
+        student.date_of_birth = date(1981, 2, 4)
+        self.app['students'].addStudent(student)
+        self.student_id = student.student_id
+        self.student = self.app['students'][self.student_id]
+
+        # Set password
+        IUserAccount(
+            self.app['students'][self.student_id]).setPassword('spwd')
+
+        self.login_path = 'http://localhost/app/login'
+        self.container_path = 'http://localhost/app/students'
+        self.manage_container_path = self.container_path + '/@@manage'
+        self.add_student_path = self.container_path + '/addstudent'
+        self.student_path = self.container_path + '/' + self.student_id
+        self.manage_student_path = self.student_path + '/manage_base'
+        self.trigtrans_path = self.student_path + '/trigtrans'
+        self.clearance_path = self.student_path + '/view_clearance'
+        self.personal_path = self.student_path + '/view_personal'
+        self.edit_clearance_path = self.student_path + '/cedit'
+        self.manage_clearance_path = self.student_path + '/manage_clearance'
+        self.edit_personal_path = self.student_path + '/edit_personal'
+        self.manage_personal_path = self.student_path + '/manage_personal'
+        self.studycourse_path = self.student_path + '/studycourse'
+        self.payments_path = self.student_path + '/payments'
+        self.acco_path = self.student_path + '/accommodation'
+        self.history_path = self.student_path + '/history'
+
+        # Create 5 access codes with prefix'PWD'
+        pin_container = self.app['accesscodes']
+        pin_container.createBatch(
+            datetime.utcnow(), 'some_userid', 'PWD', 9.99, 5)
+        pins = pin_container['PWD-1'].values()
+        self.pwdpins = [x.representation for x in pins]
+        self.existing_pwdpin = self.pwdpins[0]
+        parts = self.existing_pwdpin.split('-')[1:]
+        self.existing_pwdseries, self.existing_pwdnumber = parts
+        # Create 5 access codes with prefix 'CLR'
+        pin_container.createBatch(
+            datetime.now(), 'some_userid', 'CLR', 9.99, 5)
+        pins = pin_container['CLR-1'].values()
+        pins[0].owner = u'Hans Wurst'
+        self.existing_clrac = pins[0]
+        self.existing_clrpin = pins[0].representation
+        parts = self.existing_clrpin.split('-')[1:]
+        self.existing_clrseries, self.existing_clrnumber = parts
+        # Create 2 access codes with prefix 'HOS'
+        pin_container.createBatch(
+            datetime.now(), 'some_userid', 'HOS', 9.99, 2)
+        pins = pin_container['HOS-1'].values()
+        self.existing_hosac = pins[0]
+        self.existing_hospin = pins[0].representation
+        parts = self.existing_hospin.split('-')[1:]
+        self.existing_hosseries, self.existing_hosnumber = parts
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = u'CERT1'
+        self.certificate.application_category = 'basic'
+        self.certificate.study_mode = 'ug_ft'
+        self.certificate.start_level = 100
+        self.certificate.end_level = 500
+        self.certificate.school_fee_1 = 40000.0
+        self.certificate.school_fee_2 = 20000.0
+        self.app['faculties']['fac1'] = Faculty(code=u'fac1')
+        self.app['faculties']['fac1']['dep1'] = Department(code=u'dep1')
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        self.course = createObject('waeup.Course')
+        self.course.code = 'COURSE1'
+        self.course.semester = 1
+        self.course.credits = 10
+        self.course.passmark = 40
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        self.app['faculties']['fac1']['dep1'].certificates[
+            'CERT1'].addCertCourse(self.course, level=100)
+
+        # Configure university and hostels
+        self.app['hostels'].accommodation_states = ['admitted']
+        self.app['hostels'].accommodation_session = 2004
+        delta = timedelta(days=10)
+        self.app['hostels'].startdate = datetime.now(pytz.utc) - delta
+        self.app['hostels'].enddate = datetime.now(pytz.utc) + delta
+        self.app['configuration'].carry_over = True
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2004
+        configuration.clearance_fee = 3456.0
+        configuration.transcript_fee = 4567.0
+        configuration.booking_fee = 123.4
+        configuration.maint_fee = 987.0
+        configuration.transfer_fee = 456.0
+        configuration.late_registration_fee = 345.0
+        self.app['configuration'].addSessionConfiguration(configuration)
+
+        # Create a hostel with two beds
+        hostel = Hostel()
+        hostel.hostel_id = u'hall-1'
+        hostel.hostel_name = u'Hall 1'
+        hostel.maint_fee = 876.0
+        self.app['hostels'].addHostel(hostel)
+        bed = Bed()
+        bed.bed_id = u'hall-1_A_101_A'
+        bed.bed_number = 1
+        bed.owner = NOT_OCCUPIED
+        bed.bed_type = u'regular_male_fr'
+        self.app['hostels'][hostel.hostel_id].addBed(bed)
+        bed = Bed()
+        bed.bed_id = u'hall-1_A_101_B'
+        bed.bed_number = 2
+        bed.owner = NOT_OCCUPIED
+        bed.bed_type = u'regular_female_fr'
+        self.app['hostels'][hostel.hostel_id].addBed(bed)
+
+        # Set study course attributes of test student
+        self.student['studycourse'].certificate = self.certificate
+        self.student['studycourse'].current_session = 2004
+        self.student['studycourse'].entry_session = 2004
+        self.student['studycourse'].current_verdict = 'A'
+        self.student['studycourse'].current_level = 100
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(StudentsFullSetup, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+
+class StudentsContainerUITests(StudentsFullSetup):
+    # Tests for StudentsContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def test_anonymous_access(self):
+        # Anonymous users can't access students containers
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.container_path)
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_container_path)
+        return
+
+    def test_manage_access(self):
+        # Managers can access the view page of students
+        # containers and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.container_path)
+        self.browser.getLink("Manage students section").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_container_path)
+        return
+
+    def test_add_search_delete_students(self):
+        # Managers can add search and remove students
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getLink("Add student").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_student_path)
+        self.browser.getControl(name="form.firstname").value = 'Bob'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl("Create student").click()
+        self.assertTrue(
+            'Registration number exists already' in self.browser.contents)
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Create student").click()
+        self.assertTrue('Student record created' in self.browser.contents)
+
+        # Registration and matric numbers must be unique
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Registration number exists...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.reg_number").value = '789'
+        self.browser.getControl(name="form.matric_number").value = '234'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Matriculation number exists...',
+                           self.browser.contents)
+
+        # We can find a student with a certain student_id
+        self.browser.open(self.container_path)
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Empty search string' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['student_id']
+        self.browser.getControl(name="searchterm").value = self.student_id
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find a student in a certain session
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['current_session']
+        self.browser.getControl(name="searchterm").value = '2004'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        # Session fileds require integer values
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['current_session']
+        self.browser.getControl(name="searchterm").value = '2004/2005'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Only year dates allowed' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['current_session']
+        self.browser.getControl(name="searchterm").value = '2004/2005'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Only year dates allowed' in self.browser.contents)
+
+        # We can find a student in a certain study_mode
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['current_mode']
+        self.browser.getControl(name="searchterm").value = 'ug_ft'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find a student in a certain department
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['depcode']
+        self.browser.getControl(name="searchterm").value = 'dep1'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find a student by searching for all kind of name parts
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Empty search string' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Anna Tester'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Anna'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'An'
+        self.browser.getControl("Find student(s)").click()
+        self.assertFalse('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'An*'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'tester'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester Ana'
+        self.browser.getControl("Find student(s)").click()
+        self.assertFalse('Anna Tester' in self.browser.contents)
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Tester Anna'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        # The old searchterm will be used again
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # We can find suspended students
+        self.student.suspended = True
+        notify(grok.ObjectModifiedEvent(self.student))
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['suspended']
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['suspended']
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        # The catalog is informed when studycourse objects have been
+        # edited
+        self.browser.open(self.studycourse_path + '/manage')
+        self.browser.getControl(name="form.current_session").value = ['2010']
+        self.browser.getControl(name="form.entry_session").value = ['2010']
+        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
+        self.browser.getControl("Save").click()
+
+        # We can find the student in the new session
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['current_session']
+        self.browser.getControl(name="searchterm").value = '2010'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        ctrl = self.browser.getControl(name='entries')
+        ctrl.getControl(value=self.student_id).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['student_id']
+        self.browser.getControl(name="searchterm").value = self.student_id
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('No student found' in self.browser.contents)
+
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['student_id']
+        self.browser.getControl(name="searchterm").value = self.student_id
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('No student found' in self.browser.contents)
+        return
+
+    def test_add_graduated_students(self):
+        # Managers can add search and remove students
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getLink("Add student").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.add_student_path)
+        self.browser.getControl(name="form.firstname").value = 'Bob'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '1234'
+        self.browser.getControl("Create graduated student").click()
+        self.assertTrue('Graduated student record created' in self.browser.contents)
+        self.assertEqual(self.app['students']['K1000001'].state, 'graduated')
+        self.browser.open("http://localhost/app/students/K1000001/history")
+        self.assertTrue("State 'graduated' set by Manager" in self.browser.contents)
+        return
+
+
+class OfficerUITests(StudentsFullSetup):
+    # Tests for Student class views and pages
+
+    def test_student_properties(self):
+        self.student['studycourse'].current_level = 100
+        self.assertEqual(self.student.current_level, 100)
+        self.student['studycourse'].current_session = 2011
+        self.assertEqual(self.student.current_session, 2011)
+        self.student['studycourse'].current_verdict = 'A'
+        self.assertEqual(self.student.current_verdict, 'A')
+        return
+
+    def test_studylevelmanagepage(self):
+        studylevel = StudentStudyLevel()
+        studylevel.level = 100
+        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
+        self.student['studycourse'].addStudentStudyLevel(cert, studylevel)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.studycourse_path + '/100/manage')
+        self.assertEqual(
+            self.browser.url, self.studycourse_path + '/100/manage')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+
+    def test_basic_auth(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app')
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        # But we are still logged in since we've used basic
+        # authentication here.  Wikipedia says: Existing browsers
+        # retain authentication information until the tab or browser
+        # is closed or the user clears the history.  HTTP does not
+        # provide a method for a server to direct clients to discard
+        # these cached credentials. This means that there is no
+        # effective way for a server to "log out" the user without
+        # closing the browser. This is a significant defect that
+        # requires browser manufacturers to support a "logout" user
+        # interface element ...
+        self.assertTrue('Manager' in self.browser.contents)
+
+    def test_basic_auth_base64(self):
+        auth_token = base64.b64encode('mgr:mgrpw')
+        self.browser.addHeader('Authorization', 'Basic %s' % auth_token)
+        self.browser.open(self.manage_container_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+
+    def test_manage_access(self):
+        # Managers can access the pages of students
+        # and can perform actions
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.student_path)
+        self.browser.getLink("Trigger").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # Managers can trigger transitions
+        self.browser.getControl(name="transition").value = ['admit']
+        self.browser.getControl("Save").click()
+        # Managers can edit base
+        self.browser.open(self.student_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.url, self.manage_student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.reg_number").value = '345'
+        self.browser.getControl(name="password").value = 'secret'
+        self.browser.getControl(name="control_password").value = 'secret'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.clearance_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_clearance_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+
+        self.browser.open(self.student_path)
+        self.browser.getLink("Personal Data").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.personal_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_personal_path)
+        self.browser.open(self.personal_path)
+        self.assertTrue('Updated' in self.browser.contents)
+        self.browser.getLink("Edit").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.edit_personal_path)
+        self.browser.getControl("Save").click()
+        # perm_address is required in IStudentPersonalEdit
+        self.assertMatches('...Required input is missing...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.perm_address").value = 'My address!'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...',
+                           self.browser.contents)
+
+        # Managers can browse all subobjects
+        self.browser.open(self.student_path)
+        self.browser.getLink("Payments").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.payments_path)
+        self.browser.open(self.student_path)
+        self.browser.getLink("Accommodation").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        # We have been redirected to the manage page
+        self.assertEqual(self.browser.url, self.acco_path + '/manage')
+        self.browser.open(self.student_path)
+        self.browser.getLink("History").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.history_path)
+        self.assertMatches('...Admitted by Manager...',
+                           self.browser.contents)
+        # Only the Application Slip does not exist
+        self.assertFalse('Application Slip' in self.browser.contents)
+        return
+
+    def test_flash_notice(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.assertFalse('alert alert-warning' in self.browser.contents)
+        self.student.flash_notice = u'Happy Birthday!'
+        self.browser.open(self.student_path)
+        self.assertTrue(
+            '<div><div class="alert alert-warning">Happy Birthday!</div>'
+            in self.browser.contents)
+        return
+
+    def test_manage_contact_student(self):
+        # Managers can contact student
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        # Remove required FieldProperty attribute first ...
+        delattr(Student, 'email')
+        # ... and replace by None
+        self.student.email = None
+        # Now we have to add  the FieldProperty attribute again. Otherwise
+        # many other tests below will fail.
+        iface = list(implementedBy(Student))[0]
+        field_property = FieldProperty(iface['email'])
+        setattr(Student, 'email', field_property)
+        self.browser.open(self.student_path)
+        self.browser.getLink("Send email").click()
+        self.browser.getControl(
+            name="form.subject").value = 'Important subject'
+        self.browser.getControl(name="form.body").value = 'Hello!'
+        self.browser.getControl("Send message now").click()
+        self.assertTrue(
+            'An smtp server error occurred' in self.browser.contents)
+        self.student.email = 'xx@yy.zz'
+        self.browser.getControl("Send message now").click()
+        self.assertTrue('Your message has been sent' in self.browser.contents)
+        return
+
+    def test_manage_remove_department(self):
+        # Lazy student is studying CERT1
+        lazystudent = Student()
+        lazystudent.firstname = u'Lazy'
+        lazystudent.lastname = u'Student'
+        self.app['students'].addStudent(lazystudent)
+        student_id = lazystudent.student_id
+        student_path = self.container_path + '/' + student_id
+        lazystudent['studycourse'].certificate = self.certificate
+        notify(grok.ObjectModifiedEvent(lazystudent))
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(student_path + '/studycourse')
+        self.assertTrue('CERT1' in self.browser.contents)
+        # After some years the department is removed
+        del self.app['faculties']['fac1']['dep1']
+        # So CERT1 does no longer exist and lazy student's
+        # certificate reference is removed too
+        self.browser.open(student_path + '/studycourse')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, student_path + '/studycourse')
+        self.assertFalse('CERT1' in self.browser.contents)
+        self.assertMatches('...<div>--</div>...',
+                           self.browser.contents)
+
+    def test_manage_upload_file(self):
+        # Managers can upload a file via the StudentClearanceManageFormPage
+        # The image is stored even if form has errors
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_clearance_path)
+        # No birth certificate has been uploaded yet
+        # Browsing the link shows a placerholder image
+        self.browser.open('birth_certificate')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+        # Create a pseudo image file and select it to be uploaded in form
+        # as birth certificate
+        self.browser.open(self.manage_clearance_path)
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
+        # The Save action does not upload files
+        self.browser.getControl("Save").click()  # submit form
+        self.assertFalse(
+            '<a target="image" href="birth_certificate">'
+            in self.browser.contents)
+        # ... but the correct upload submit button does
+        image = open(SAMPLE_IMAGE)
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        # There is a correct <img> link included
+        self.assertTrue(
+            'href="http://localhost/app/students/K1000000/birth_certificate"'
+            in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open('birth_certificate')
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), 2787)
+        # We can't reupload a file. The existing file must be deleted first.
+        self.browser.open(self.manage_clearance_path)
+        self.assertFalse(
+            'upload_birthcertificateupload' in self.browser.contents)
+        # File must be deleted first
+        self.browser.getControl(name='delete_birthcertificateupload').click()
+        self.assertTrue(
+            'birth_certificate deleted' in self.browser.contents)
+        # Uploading a file which is bigger than 250k will raise an error
+        big_image = StringIO(open(SAMPLE_IMAGE, 'rb').read() * 100)
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(big_image, filename='my_birth_certificate.jpg')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue(
+            'Uploaded file is too big' in self.browser.contents)
+        # we do not rely on filename extensions given by uploaders
+        image = open(SAMPLE_IMAGE, 'rb')  # a jpg-file
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        # tell uploaded file is bmp
+        file_ctrl.add_file(image, filename='my_birth_certificate.bmp')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue(
+            # jpg file was recognized
+            'File birth_certificate.jpg uploaded.' in self.browser.contents)
+        # Delete file again
+        self.browser.getControl(name='delete_birthcertificateupload').click()
+        self.assertTrue(
+            'birth_certificate deleted' in self.browser.contents)
+        # File names must meet several conditions
+        bmp_image = open(SAMPLE_IMAGE_BMP, 'rb')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(bmp_image, filename='my_birth_certificate.bmp')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue(
+            'Only the following extensions are allowed'
+            in self.browser.contents)
+
+        # Managers can upload a file via the StudentBaseManageFormPage
+        self.browser.open(self.manage_student_path)
+        image = open(SAMPLE_IMAGE_BMP, 'rb')
+        ctrl = self.browser.getControl(name='passportuploadmanage')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_photo.bmp')
+        self.browser.getControl(
+            name='upload_passportuploadmanage').click()
+        self.assertTrue(
+            'jpg file format expected' in self.browser.contents)
+        ctrl = self.browser.getControl(name='passportuploadmanage')
+        file_ctrl = ctrl.mech_control
+        image = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl.add_file(image, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passportuploadmanage').click()
+        self.assertTrue(
+            'src="http://localhost/app/students/K1000000/passport.jpg"'
+            in self.browser.contents)
+        # We remove the passport file again
+        self.browser.open(self.manage_student_path)
+        self.browser.getControl('Delete').click()
+        self.browser.open(self.student_path + '/clearance_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        # We want to see the signature fields.
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.open(self.student_path + '/clearance_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        path = os.path.join(samples_dir(), 'clearance_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF clearance_slip.pdf written to %s" % path
+
+    def test_manage_course_lists(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.browser.getLink("Study Course").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.studycourse_path)
+        self.assertTrue('Undergraduate Full-Time' in self.browser.contents)
+        self.browser.getLink("Manage").click()
+        self.assertTrue('Manage study course' in self.browser.contents)
+        # Before we can select a level, the certificate must
+        # be selected and saved
+        self.browser.getControl(name="form.certificate").value = ['CERT1']
+        self.browser.getControl(name="form.current_session").value = ['2004']
+        self.browser.getControl(name="form.current_verdict").value = ['A']
+        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
+        self.browser.getControl("Save").click()
+        # Now we can save also the current level which depends on start and end
+        # level of the certificate
+        self.browser.getControl(name="form.current_level").value = ['100']
+        self.browser.getControl("Save").click()
+        # Managers can add and remove any study level (course list)
+        self.browser.getControl(name="addlevel").value = ['100']
+        self.browser.getControl("Add study level").click()
+        self.assertMatches(
+            '...You must select a session...', self.browser.contents)
+        self.browser.getControl(name="addlevel").value = ['100']
+        self.browser.getControl(name="level_session").value = ['2004']
+        self.browser.getControl("Add study level").click()
+        self.assertMatches('...<span>100</span>...', self.browser.contents)
+        self.assertEqual(self.student['studycourse']['100'].level, 100)
+        self.assertEqual(
+            self.student['studycourse']['100'].level_session, 2004)
+        self.browser.getControl(name="addlevel").value = ['100']
+        self.browser.getControl(name="level_session").value = ['2004']
+        self.browser.getControl("Add study level").click()
+        self.assertMatches('...This level exists...', self.browser.contents)
+        self.browser.getControl("Remove selected").click()
+        self.assertMatches(
+            '...No study level selected...', self.browser.contents)
+        self.browser.getControl(name="val_id").value = ['100']
+        self.browser.getControl(name="level_session").value = ['2004']
+        self.browser.getControl("Remove selected").click()
+        self.assertMatches('...Successfully removed...', self.browser.contents)
+        # Removing levels is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.StudyCourseManageFormPage '
+            '- K1000000 - removed: 100' in logcontent)
+        # Add level again
+        self.browser.getControl(name="addlevel").value = ['100']
+        self.browser.getControl(name="level_session").value = ['2004']
+        self.browser.getControl("Add study level").click()
+
+        # Managers can view and manage course lists
+        self.browser.getLink("100").click()
+        self.assertMatches(
+            '...: 100 (Year 1)...', self.browser.contents)
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.level_session").value = ['2002']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Remove selected").click()
+        self.assertMatches('...No ticket selected...', self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='COURSE1').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+        # Removing course tickets is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.StudyLevelManageFormPage '
+            '- K1000000 - level 100 - removed: COURSE1' in logcontent)
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.course.credits = 100
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches(
+            '...Maximum credits exceeded...', self.browser.contents)
+        self.course.credits = 10
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Successfully added' in self.browser.contents)
+        # We can do the same by adding the course on the manage page directly
+        del self.student['studycourse']['100']['COURSE1']
+        self.browser.getControl(name="course").value = 'COURSE1'
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Successfully added' in self.browser.contents)
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('The ticket exists' in self.browser.contents)
+        self.browser.getControl("Cancel").click()
+        self.browser.getLink("COURSE1").click()
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        # Grade and weight have been determined
+        self.browser.open(self.studycourse_path + '/100/COURSE1')
+        self.assertFalse('Grade' in self.browser.contents)
+        self.assertFalse('Weight' in self.browser.contents)
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.studycourse_path + '/100/COURSE1')
+        self.assertTrue('Grade' in self.browser.contents)
+        self.assertTrue('Weight' in self.browser.contents)
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].grade, 'C')
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].weight, 3)
+        # We add another ticket to check if GPA will be correctly
+        # calculated (and rounded)
+        courseticket = createObject('waeup.CourseTicket')
+        courseticket.code = 'ANYCODE'
+        courseticket.title = u'Any TITLE'
+        courseticket.credits = 13
+        courseticket.score = 66
+        courseticket.semester = 1
+        courseticket.dcode = u'ANYDCODE'
+        courseticket.fcode = u'ANYFCODE'
+        self.student['studycourse']['100']['COURSE2'] = courseticket
+        self.browser.open(self.student_path + '/studycourse/100')
+        # total credits
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[1], 23)
+        # weigheted credits = 3 * 10 + 4 * 13
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[2], 82.0)
+        # sgpa = 82 / 23
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[0],
+            3.5652173913043477)
+        # Carry-over courses will be collected when next level is created
+        self.browser.open(self.student_path + '/studycourse/manage')
+        # Add next level
+        self.student['studycourse']['100']['COURSE1'].score = 10
+        self.browser.getControl(name="addlevel").value = ['200']
+        self.browser.getControl(name="level_session").value = ['2005']
+        self.browser.getControl("Add study level").click()
+        self.browser.getLink("200").click()
+        self.assertMatches(
+            '...: 200 (Year 2)...', self.browser.contents)
+        # Since COURSE1 has score 10 it becomes a carry-over course
+        # in level 200
+        self.assertEqual(
+            sorted(self.student['studycourse']['200'].keys()), [u'COURSE1'])
+        self.assertTrue(
+            self.student['studycourse']['200']['COURSE1'].carry_over)
+        # Passed and failed courses have been counted
+        self.assertEqual(
+            self.student['studycourse']['100'].passed_params,
+            (1, 1, 13, 10, 'COURSE1 ', ''))
+        self.assertEqual(
+            self.student['studycourse']['200'].passed_params,
+            (0, 0, 0, 0, '', 'COURSE1 '))
+        # And also cumulative params can be calculated. Meanwhile we have the
+        # following courses: COURSE1 and COURSE2 in level 100 and
+        # COURSE1 as carry-over course in level 200.
+        self.assertEqual(
+            self.student['studycourse']['100'].cumulative_params,
+            (2.260869565217391, 23, 52.0, 23, 13))
+        # COURSE1 in level 200 is not taken into consideration
+        # when calculating the gpa.
+        self.assertEqual(
+            self.student['studycourse']['200'].cumulative_params,
+            (2.260869565217391, 23, 52.0, 33, 13))
+        return
+
+    def test_gpa_calculation_with_carryover(self):
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2005
+        self.student['studycourse'].entry_mode = 'ug_ft'
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        # First course has been added automatically.
+        # Set score above passmark.
+        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark + 1
+        # GPA is 1.
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[0], 1.0)
+        # Set score below passmark.
+        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark - 1
+        # GPA is still 0.
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[0], 0.0)
+        studylevel2 = createObject(u'waeup.StudentStudyLevel')
+        studylevel2.level = 200
+        studylevel2.level_session = 2006
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel2)
+        # Carry-over course has been autonatically added.
+        studylevel2['COURSE1'].score = 66
+        # The score of the carry-over course is now used for calculation of the
+        # GPA at level 100 ...
+        self.assertEqual(
+            self.student['studycourse']['100'].gpa_params_rectified[0], 4.0)
+        # ... but not at level 200
+        self.assertEqual(
+            self.student['studycourse']['200'].gpa_params_rectified[0], 0.0)
+        return
+
+    def test_manage_payments(self):
+        # Managers can add online school fee payment tickets
+        # if certain requirements are met
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
+        payment_url = self.browser.url
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            ' zope.mgr - students.browser.OnlinePaymentAddFormPage - '
+            'K1000000 - added: %s' % value
+            in logcontent)
+        # The pdf payment slip can't yet be opened
+        #self.browser.open(payment_url + '/payment_slip.pdf')
+        #self.assertMatches('...Ticket not yet paid...',
+        #                   self.browser.contents)
+
+        # The same payment (with same p_item, p_session and
+        # p_category) can be initialized a second time if the former
+        # ticket is not yet paid.
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+
+        # The ticket can be found in the payments_catalog
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
+        self.assertEqual(len(results), 2)
+        self.assertTrue(results[0] is self.student['payments'][value])
+        # Managers can approve the payment
+        # If, by some reason, the state has already changed,
+        # an access code is created after approval.
+        IWorkflowState(self.student).setState('school fee paid')
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']), 0)
+        self.browser.open(payment_url)
+        self.browser.getLink("Approve payment").click()
+        self.assertMatches(
+            '...Payment approved...', self.browser.contents)
+        # Approval is logged in students.log ...
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.OnlinePaymentApproveView '
+            '- K1000000 - schoolfee payment approved'
+            in logcontent)
+        # ... and in payments.log
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'payments.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
+            in logcontent)
+        # The authorized amount has been stored in the new access code
+        self.assertEqual(
+            self.app['accesscodes']['SFE-0'].values()[0].cost, 40000.0)
+
+        # The catalog has been updated
+        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
+        self.assertTrue(len(results), 0)
+        results = list(cat.searchResults(p_state=('paid', 'paid')))
+        self.assertTrue(len(results), 1)
+        self.assertTrue(results[0] is self.student['payments'][value])
+
+        # Payments can't be approved twice
+        self.browser.open(payment_url + '/approve')
+        self.assertMatches('...This ticket has already been paid...',
+                          self.browser.contents)
+
+        # Now the first ticket is paid and no more ticket of same type
+        # (with same p_item, p_session and p_category) can be added.
+        # First we have to reset the workflow state.
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches(
+            '...This type of payment has already been made...',
+            self.browser.contents)
+
+        # Managers can open the pdf payment slip
+        self.browser.open(payment_url)
+        self.browser.getLink("Download payment slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+
+        # Managers can remove online school fee payment tickets
+        self.browser.open(self.payments_path)
+        self.browser.getControl("Remove selected").click()
+        self.assertMatches('...No payment selected...', self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+        # Managers can add online clearance payment tickets
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['clearance']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+
+        # Managers can approve the payment
+        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[1] # The clearance payment is the second in the table
+        self.browser.getLink(value).click()
+        self.browser.open(self.browser.url + '/approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+        expected = '''...
+        <td>
+          <span>Paid</span>
+        </td>...'''
+        self.assertMatches(expected,self.browser.contents)
+        # The new CLR-0 pin has been created
+        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
+        pin = self.app['accesscodes']['CLR-0'].keys()[0]
+        ac = self.app['accesscodes']['CLR-0'][pin]
+        self.assertEqual(ac.owner, self.student_id)
+        self.assertEqual(ac.cost, 3456.0)
+
+        # Managers can add online transcript payment tickets
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['transcript']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+
+        # Managers can approve the payment
+        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[2] # The clearance payment is the third in the table
+        self.browser.getLink(value).click()
+        self.browser.open(self.browser.url + '/approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+        expected = '''...
+        <td>
+          <span>Paid</span>
+        </td>...'''
+        self.assertMatches(expected,self.browser.contents)
+        # The new CLR-0 pin has been created
+        self.assertEqual(len(self.app['accesscodes']['TSC-0']),1)
+        pin = self.app['accesscodes']['TSC-0'].keys()[0]
+        ac = self.app['accesscodes']['TSC-0'][pin]
+        self.assertEqual(ac.owner, self.student_id)
+        self.assertEqual(ac.cost, 4567.0)
+        return
+
+    def test_add_transfer_payment(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['transfer']
+        self.browser.getControl(name="new_programme").value = 'my new study course'
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...my new study course...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].p_item, u'my new study course')
+
+    def test_manage_payments_bypass_ac_creation(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        payment_url = self.browser.url
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        # The ticket can be found in the payments_catalog
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
+        self.assertTrue(len(results), 1)
+        self.assertTrue(results[0] is self.student['payments'][value])
+        # Managers can approve the payment
+        self.browser.open(payment_url)
+        self.browser.getLink("Approve payment").click()
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+        # Approval is logged in students.log ...
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.OnlinePaymentApproveView '
+            '- K1000000 - schoolfee payment approved'
+            in logcontent)
+        # ... and in payments.log
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'payments.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
+            in logcontent)
+        # Student is in state school fee paid, no activation
+        # code was necessary.
+        self.assertEqual(self.student.state, 'school fee paid')
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
+        return
+
+    def test_payment_disabled(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.app['configuration']['2004'].payment_disabled = ['sf_all']
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...This category of payments has been disabled...',
+                           self.browser.contents)
+        return
+
+    def test_manage_balance_payments(self):
+
+        # Login
+        #self.browser.open(self.login_path)
+        #self.browser.getControl(name="form.login").value = self.student_id
+        #self.browser.getControl(name="form.password").value = 'spwd'
+        #self.browser.getControl("Login").click()
+
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+
+        # Managers can add balance school fee payment tickets in any state.
+        IWorkflowState(self.student).setState('courses registered')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add balance payment ticket").click()
+
+        # Balance payment form is provided
+        self.assertEqual(self.student.current_session, 2004)
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl(name="form.balance_session").value = ['2004']
+        self.browser.getControl(name="form.balance_level").value = ['300']
+        self.browser.getControl(name="form.balance_amount").value = '-567.8'
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Amount must be greater than 0...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.balance_amount").value = '0'
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Amount must be greater than 0...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.balance_amount").value = '567.8'
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 567.8)
+        # Payment attributes are properly set
+        self.assertEqual(self.student['payments'][value].p_session, 2004)
+        self.assertEqual(self.student['payments'][value].p_level, 300)
+        self.assertEqual(self.student['payments'][value].p_item, u'Balance')
+        self.assertEqual(self.student['payments'][value].p_category, 'schoolfee')
+        # Adding payment tickets is logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - students.browser.BalancePaymentAddFormPage '
+                        '- K1000000 - added: %s' % value in logcontent)
+
+    def test_manage_accommodation(self):
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        # Managers can add online booking fee payment tickets and open the
+        # callback view (see test_manage_payments)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
+        # If student is not in accommodation session, payment cannot be processed
+        self.app['hostels'].accommodation_session = 2011
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Your current session does not match...',
+                           self.browser.contents)
+        self.app['hostels'].accommodation_session = 2004
+        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.browser.open(self.browser.url + '/approve')
+        # The new HOS-0 pin has been created
+        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
+        pin = self.app['accesscodes']['HOS-0'].keys()[0]
+        ac = self.app['accesscodes']['HOS-0'][pin]
+        self.assertEqual(ac.owner, self.student_id)
+        parts = pin.split('-')[1:]
+        sfeseries, sfenumber = parts
+        # Managers can use HOS code and book a bed space with it
+        self.browser.open(self.acco_path)
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...You are in the wrong...',
+                           self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('admit')
+        # An existing HOS code can only be used if students
+        # are in accommodation session
+        self.student['studycourse'].current_session = 2003
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...Your current session does not match...',
+                           self.browser.contents)
+        self.student['studycourse'].current_session = 2004
+        # All requirements are met and ticket can be created
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...Activation Code:...',
+                           self.browser.contents)
+        self.browser.getControl(name="ac_series").value = sfeseries
+        self.browser.getControl(name="ac_number").value = sfenumber
+        self.browser.getControl("Create bed ticket").click()
+        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
+                           self.browser.contents)
+        # Bed has been allocated
+        bed1 = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        self.assertTrue(bed1.owner == self.student_id)
+        # BedTicketAddPage is now blocked
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...You already booked a bed space...',
+            self.browser.contents)
+        # The bed ticket displays the data correctly
+        self.browser.open(self.acco_path + '/2004')
+        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
+                           self.browser.contents)
+        self.assertMatches('...2004/2005...', self.browser.contents)
+        self.assertMatches('...regular_male_fr...', self.browser.contents)
+        self.assertMatches('...%s...' % pin, self.browser.contents)
+        # Booking is properly logged
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - students.browser.BedTicketAddPage '
+            '- K1000000 - booked: hall-1_A_101_A' in logcontent)
+        # Managers can relocate students if the student's bed_type has changed
+        self.browser.getLink("Relocate student").click()
+        self.assertMatches(
+            "...Student can't be relocated...", self.browser.contents)
+        self.student.sex = u'f'
+        self.browser.getLink("Relocate student").click()
+        self.assertMatches(
+            "...Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
+        self.assertTrue(bed1.owner == NOT_OCCUPIED)
+        bed2 = self.app['hostels']['hall-1']['hall-1_A_101_B']
+        self.assertTrue(bed2.owner == self.student_id)
+        self.assertTrue(self.student['accommodation'][
+            '2004'].bed_type == u'regular_female_fr')
+        # Relocation is properly logged
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - students.accommodation.BedTicket '
+            '- K1000000 - relocated: hall-1_A_101_B' in logcontent)
+        # The payment object still shows the original payment item
+        payment_id = self.student['payments'].keys()[0]
+        payment = self.student['payments'][payment_id]
+        self.assertTrue(payment.p_item == u'regular_male_fr')
+        # Managers can relocate students if the bed's bed_type has changed
+        bed1.bed_type = u'regular_female_fr'
+        bed2.bed_type = u'regular_male_fr'
+        notify(grok.ObjectModifiedEvent(bed1))
+        notify(grok.ObjectModifiedEvent(bed2))
+        self.browser.getLink("Relocate student").click()
+        self.assertMatches(
+            "...Student relocated...", self.browser.contents)
+        self.assertMatches(
+            "... Hall 1, Block A, Room 101, Bed A...", self.browser.contents)
+        self.assertMatches(bed1.owner, self.student_id)
+        self.assertMatches(bed2.owner, NOT_OCCUPIED)
+        # Managers can't relocate students if bed is reserved
+        self.student.sex = u'm'
+        bed1.bed_type = u'regular_female_reserved'
+        notify(grok.ObjectModifiedEvent(bed1))
+        self.browser.getLink("Relocate student").click()
+        self.assertMatches(
+            "...Students in reserved beds can't be relocated...",
+            self.browser.contents)
+        # Managers can relocate students if booking has been cancelled but
+        # other bed space has been manually allocated after cancellation
+        old_owner = bed1.releaseBed()
+        self.assertMatches(old_owner, self.student_id)
+        bed2.owner = self.student_id
+        self.browser.open(self.acco_path + '/2004')
+        self.assertMatches(
+            "...booking cancelled...", self.browser.contents)
+        self.browser.getLink("Relocate student").click()
+        # We didn't informed the catalog therefore the new owner is not found
+        self.assertMatches(
+            "...There is no free bed in your category regular_male_fr...",
+            self.browser.contents)
+        # Now we fire the event properly
+        notify(grok.ObjectModifiedEvent(bed2))
+        self.browser.getLink("Relocate student").click()
+        self.assertMatches(
+            "...Student relocated...", self.browser.contents)
+        self.assertMatches(
+            "... Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
+          # Managers can delete bed tickets
+        self.browser.open(self.acco_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertMatches('...Successfully removed...', self.browser.contents)
+        # The bed has been properly released by the event handler
+        self.assertMatches(bed1.owner, NOT_OCCUPIED)
+        self.assertMatches(bed2.owner, NOT_OCCUPIED)
+        return
+
+    def test_manage_workflow(self):
+        # Managers can pass through the whole workflow
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        student = self.app['students'][self.student_id]
+        self.browser.open(self.trigtrans_path)
+        self.assertTrue(student.clearance_locked)
+        self.browser.getControl(name="transition").value = ['admit']
+        self.browser.getControl("Save").click()
+        self.assertTrue(student.clearance_locked)
+        self.browser.getControl(name="transition").value = ['start_clearance']
+        self.browser.getControl("Save").click()
+        self.assertFalse(student.clearance_locked)
+        self.browser.getControl(name="transition").value = ['request_clearance']
+        self.browser.getControl("Save").click()
+        self.assertTrue(student.clearance_locked)
+        self.browser.getControl(name="transition").value = ['clear']
+        self.browser.getControl("Save").click()
+        # Managers approve payment, they do not pay
+        self.assertFalse('pay_first_school_fee' in self.browser.contents)
+        self.browser.getControl(
+            name="transition").value = ['approve_first_school_fee']
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="transition").value = ['reset6']
+        self.browser.getControl("Save").click()
+        # In state returning the pay_school_fee transition triggers some 
+        # changes of attributes
+        self.browser.getControl(name="transition").value = ['approve_school_fee']
+        self.browser.getControl("Save").click()
+        self.assertEqual(student['studycourse'].current_session, 2005) # +1
+        self.assertEqual(student['studycourse'].current_level, 200) # +100
+        self.assertEqual(student['studycourse'].current_verdict, '0') # 0 = Zero = not set
+        self.assertEqual(student['studycourse'].previous_verdict, 'A')
+        self.browser.getControl(name="transition").value = ['register_courses']
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="transition").value = ['validate_courses']
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="transition").value = ['return']
+        self.browser.getControl("Save").click()
+        return
+
+    def test_manage_pg_workflow(self):
+        # Managers can pass through the whole workflow
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.trigtrans_path)
+        self.assertTrue('<option value="reset6">' in self.browser.contents)
+        self.assertTrue('<option value="register_courses">' in self.browser.contents)
+        self.assertTrue('<option value="reset5">' in self.browser.contents)
+        self.certificate.study_mode = 'pg_ft'
+        self.browser.open(self.trigtrans_path)
+        self.assertFalse('<option value="reset6">' in self.browser.contents)
+        self.assertFalse('<option value="register_courses">' in self.browser.contents)
+        self.assertTrue('<option value="reset5">' in self.browser.contents)
+        return
+
+    def test_manage_import(self):
+        # Managers can import student data files
+        datacenter_path = 'http://localhost/app/datacenter'
+        # Prepare a csv file for students
+        open('students.csv', 'wb').write(
+"""firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,password
+Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234,m,mypwd1
+Claus,Finau,2,1990-01-03,100001,aa@aa.ng,1234,m,mypwd1
+Brit,Berson,3,1990-01-04,100001,aa@aa.ng,1234,m,mypwd1
+""")
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(datacenter_path)
+        self.browser.getLink('Upload data').click()
+        filecontents = StringIO(open('students.csv', 'rb').read())
+        filewidget = self.browser.getControl(name='uploadfile:file')
+        filewidget.add_file(filecontents, 'text/plain', 'students.csv')
+        self.browser.getControl(name='SUBMIT').click()
+        self.browser.getLink('Process data').click()
+        self.browser.getLink("Switch maintenance mode").click()
+        button = lookup_submit_value(
+            'select', 'students_zope.mgr.csv', self.browser)
+        button.click()
+        importerselect = self.browser.getControl(name='importer')
+        modeselect = self.browser.getControl(name='mode')
+        importerselect.getControl('Student Processor').selected = True
+        modeselect.getControl(value='create').selected = True
+        self.browser.getControl('Proceed to step 3').click()
+        self.assertTrue('Header fields OK' in self.browser.contents)
+        self.browser.getControl('Perform import').click()
+        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
+        self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
+        self.assertTrue('Batch processing finished' in self.browser.contents)
+        open('studycourses.csv', 'wb').write(
+"""reg_number,matric_number,certificate,current_session,current_level
+1,,CERT1,2008,100
+,100001,CERT1,2008,100
+,100002,CERT1,2008,100
+""")
+        self.browser.open(datacenter_path)
+        self.browser.getLink('Upload data').click()
+        filecontents = StringIO(open('studycourses.csv', 'rb').read())
+        filewidget = self.browser.getControl(name='uploadfile:file')
+        filewidget.add_file(filecontents, 'text/plain', 'studycourses.csv')
+        self.browser.getControl(name='SUBMIT').click()
+        self.browser.getLink('Process data').click()
+        # Meanwhile maintenance mode is disabled again.
+        self.browser.getLink("Switch maintenance mode").click()
+        button = lookup_submit_value(
+            'select', 'studycourses_zope.mgr.csv', self.browser)
+        button.click()
+        importerselect = self.browser.getControl(name='importer')
+        modeselect = self.browser.getControl(name='mode')
+        importerselect.getControl(
+            'StudentStudyCourse Processor (update only)').selected = True
+        modeselect.getControl(value='create').selected = True
+        self.browser.getControl('Proceed to step 3').click()
+        self.assertTrue('Update mode only' in self.browser.contents)
+        self.browser.getControl('Proceed to step 3').click()
+        self.assertTrue('Header fields OK' in self.browser.contents)
+        self.browser.getControl('Perform import').click()
+        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
+        self.assertTrue('Successfully processed 2 rows'
+                        in self.browser.contents)
+        # The students are properly indexed and we can
+        # thus find a student in  the department
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="searchtype").value = ['depcode']
+        self.browser.getControl(name="searchterm").value = 'dep1'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Aaren Pieri' in self.browser.contents)
+        # We can search for a new student by name ...
+        self.browser.getControl(name="searchtype").value = ['fullname']
+        self.browser.getControl(name="searchterm").value = 'Claus'
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Claus Finau' in self.browser.contents)
+        # ... and check if the imported password has been properly set
+        ctrl = self.browser.getControl(name='entries')
+        value = ctrl.options[0]
+        claus = self.app['students'][value]
+        self.assertTrue(IUserAccount(claus).checkPassword('mypwd1'))
+        return
+
+    def init_clearance_officer(self):
+        # Create clearance officer
+        self.app['users'].addUser('mrclear', SECRET)
+        self.app['users']['mrclear'].email = 'mrclear@foo.ng'
+        self.app['users']['mrclear'].title = 'Carlo Pitter'
+        # Clearance officers need not necessarily to get
+        # the StudentsOfficer site role
+        #prmglobal = IPrincipalRoleManager(self.app)
+        #prmglobal.assignRoleToPrincipal('waeup.StudentsOfficer', 'mrclear')
+        # Assign local ClearanceOfficer role
+        self.department = self.app['faculties']['fac1']['dep1']
+        prmlocal = IPrincipalRoleManager(self.department)
+        prmlocal.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'mrclear')
+        IWorkflowState(self.student).setState('clearance started')
+        # Add another student for testing
+        other_student = Student()
+        other_student.firstname = u'Dep2'
+        other_student.lastname = u'Student'
+        self.app['students'].addStudent(other_student)
+        self.other_student_path = (
+            'http://localhost/app/students/%s' % other_student.student_id)
+        # Login as clearance officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrclear'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+
+    def test_handle_clearance_by_co(self):
+        self.init_clearance_officer()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # CO is landing on index page.
+        self.assertEqual(self.browser.url, 'http://localhost/app/index')
+        # CO can see his roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Academics Officer (view only)</div>...',
+            self.browser.contents)
+        # But not his local role ...
+        self.assertFalse('Clearance Officer' in self.browser.contents)
+        # ... because we forgot to notify the department that the local role
+        # has changed.
+        notify(LocalRoleSetEvent(
+            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
+            granted=True))
+        self.browser.open('http://localhost/app/users/mrclear/my_roles')
+        self.assertTrue('Clearance Officer' in self.browser.contents)
+        self.assertMatches(
+            '...<a href="http://localhost/app/faculties/fac1/dep1">...',
+            self.browser.contents)
+        # CO can view the student ...
+        self.browser.open(self.clearance_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.clearance_path)
+        # ... but not other students.
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.other_student_path)
+        # Clearance is disabled for this session.
+        self.browser.open(self.clearance_path)
+        self.assertFalse('Clear student' in self.browser.contents)
+        self.browser.open(self.student_path + '/clear')
+        self.assertTrue('Clearance is disabled for this session'
+            in self.browser.contents)
+        self.app['configuration']['2004'].clearance_enabled = True
+        # Only in state clearance requested the CO does see the 'Clear' button.
+        self.browser.open(self.clearance_path)
+        self.assertFalse('Clear student' in self.browser.contents)
+        self.browser.open(self.student_path + '/clear')
+        self.assertTrue('Student is in wrong state.'
+            in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        self.browser.open(self.clearance_path)
+        self.assertTrue('Clear student' in self.browser.contents)
+        self.browser.getLink("Clear student").click()
+        self.assertTrue('Student has been cleared' in self.browser.contents)
+        self.assertTrue('cleared' in self.browser.contents)
+        self.browser.open(self.history_path)
+        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
+        # Hide real name.
+        self.app['users']['mrclear'].public_name = 'My Public Name'
+        self.browser.open(self.clearance_path)
+        self.browser.getLink("Reject clearance").click()
+        self.assertEqual(
+            self.browser.url, self.student_path + '/reject_clearance')
+        # Type comment why.
+        self.browser.getControl(name="form.officer_comment").value = (
+            'Dear Student,\n'
+            'You did not fill properly.')
+        self.browser.getControl("Save comment").click()
+        self.assertTrue('Clearance has been annulled' in self.browser.contents)
+        url = ('http://localhost/app/students/K1000000/'
+              'contactstudent?body=Dear+Student%2C%0AYou+did+not+fill+properly.'
+              '&subject=Clearance+has+been+annulled.')
+        # CO does now see the prefilled contact form and can send a message.
+        self.assertEqual(self.browser.url, url)
+        self.assertTrue('clearance started' in self.browser.contents)
+        self.assertTrue('name="form.subject" size="20" type="text" '
+            'value="Clearance has been annulled."'
+            in self.browser.contents)
+        self.assertTrue('name="form.body" rows="10" >Dear Student,'
+            in self.browser.contents)
+        self.browser.getControl("Send message now").click()
+        self.assertTrue('Your message has been sent' in self.browser.contents)
+        # The comment has been stored ...
+        self.assertEqual(self.student.officer_comment,
+            u'Dear Student,\nYou did not fill properly.')
+        # ... and logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - mrclear - students.browser.StudentRejectClearancePage - '
+            'K1000000 - comment: Dear Student,<br>You did not fill '
+            'properly.\n' in logcontent)
+        self.browser.open(self.history_path)
+        self.assertTrue("Reset to 'clearance started' by My Public Name" in
+            self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        self.browser.open(self.clearance_path)
+        self.browser.getLink("Reject clearance").click()
+        self.browser.getControl("Save comment").click()
+        self.assertTrue('Clearance request has been rejected'
+            in self.browser.contents)
+        self.assertTrue('clearance started' in self.browser.contents)
+        # The CO can't clear students if not in state
+        # clearance requested.
+        self.browser.open(self.student_path + '/clear')
+        self.assertTrue('Student is in wrong state'
+            in self.browser.contents)
+        # The CO can go to his department throug the my_roles page ...
+        self.browser.open('http://localhost/app/users/mrclear/my_roles')
+        self.browser.getLink("http://localhost/app/faculties/fac1/dep1").click()
+        # ... and view the list of students.
+        self.browser.getLink("Show students").click()
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['200']
+        self.browser.getControl("Show").click()
+        self.assertFalse(self.student_id in self.browser.contents)
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Show").click()
+        self.assertTrue(self.student_id in self.browser.contents)
+        # The comment is indicated by 'yes'.
+        self.assertTrue('<td><span>yes</span></td>' in self.browser.contents)
+        # Check if the enquiries form is not pre-filled with officer_comment
+        # (regression test).
+        self.browser.getLink("Logout").click()
+        self.browser.open('http://localhost/app/enquiries')
+        self.assertFalse(
+            'You did not fill properly'
+            in self.browser.contents)
+        # When a student is cleared the comment is automatically deleted
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        IWorkflowInfo(self.student).fireTransition('clear')
+        self.assertEqual(self.student.officer_comment, None)
+        return
+
+    def test_handle_mass_clearance_by_co(self):
+        self.init_clearance_officer()
+        # Additional setups according to test above
+        notify(LocalRoleSetEvent(
+            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
+            granted=True))
+        self.app['configuration']['2004'].clearance_enabled = True
+        IWorkflowState(self.student).setState('clearance requested')
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(self.student))
+        # The CO can go to the department and clear all students in department
+        self.browser.open('http://localhost/app/faculties/fac1/dep1')
+        self.browser.getLink("Clear all students").click()
+        self.assertTrue('1 students have been cleared' in self.browser.contents)
+        self.browser.open(self.history_path)
+        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - mrclear - K1000000 - Cleared' in logcontent)
+        self.browser.open('http://localhost/app/faculties/fac1/dep1')
+        self.browser.getLink("Clear all students").click()
+        self.assertTrue('0 students have been cleared' in self.browser.contents)
+        return
+
+    def test_handle_courses_by_ca(self):
+        self.app['users'].addUser('mrsadvise', SECRET)
+        self.app['users']['mrsadvise'].email = 'mradvise@foo.ng'
+        self.app['users']['mrsadvise'].title = u'Helen Procter'
+        # Assign local CourseAdviser100 role for a certificate
+        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
+        prmlocal = IPrincipalRoleManager(cert)
+        prmlocal.assignRoleToPrincipal('waeup.local.CourseAdviser100', 'mrsadvise')
+        IWorkflowState(self.student).setState('school fee paid')
+        # Login as course adviser.
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrsadvise'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # CO can see his roles.
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Academics Officer (view only)</div>...',
+            self.browser.contents)
+        # But not his local role ...
+        self.assertFalse('Course Adviser' in self.browser.contents)
+        # ... because we forgot to notify the certificate that the local role
+        # has changed.
+        notify(LocalRoleSetEvent(
+            cert, 'waeup.local.CourseAdviser100', 'mrsadvise', granted=True))
+        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
+        self.assertTrue('Course Adviser 100L' in self.browser.contents)
+        self.assertMatches(
+            '...<a href="http://localhost/app/faculties/fac1/dep1/certificates/CERT1">...',
+            self.browser.contents)
+        # CA can view the student ...
+        self.browser.open(self.student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.student_path)
+        # ... but not other students.
+        other_student = Student()
+        other_student.firstname = u'Dep2'
+        other_student.lastname = u'Student'
+        self.app['students'].addStudent(other_student)
+        other_student_path = (
+            'http://localhost/app/students/%s' % other_student.student_id)
+        self.assertRaises(
+            Unauthorized, self.browser.open, other_student_path)
+        # We add study level 110 to the student's studycourse.
+        studylevel = StudentStudyLevel()
+        studylevel.level = 110
+        self.student['studycourse'].addStudentStudyLevel(
+            cert,studylevel)
+        L110_student_path = self.studycourse_path + '/110'
+        # The CA can neither see the Validate nor the Edit button.
+        self.browser.open(L110_student_path)
+        self.assertFalse('Validate courses' in self.browser.contents)
+        self.assertFalse('Edit' in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('register_courses')
+        self.browser.open(L110_student_path)
+        self.assertFalse('Validate courses' in self.browser.contents)
+        self.assertFalse('Edit' in self.browser.contents)
+        # Only in state courses registered and only if the current level
+        # corresponds with the name of the study level object
+        # the 100L CA does see the 'Validate' button but not
+        # the edit button.
+        self.student['studycourse'].current_level = 110
+        self.browser.open(L110_student_path)
+        self.assertFalse('Edit' in self.browser.contents)
+        self.assertTrue('Validate courses' in self.browser.contents)
+        # But a 100L CA does not see the button at other levels.
+        studylevel2 = StudentStudyLevel()
+        studylevel2.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            cert,studylevel2)
+        L200_student_path = self.studycourse_path + '/200'
+        self.browser.open(L200_student_path)
+        self.assertFalse('Edit' in self.browser.contents)
+        self.assertFalse('Validate courses' in self.browser.contents)
+        self.browser.open(L110_student_path)
+        self.browser.getLink("Validate courses").click()
+        self.assertTrue('Course list has been validated' in self.browser.contents)
+        self.assertTrue('courses validated' in self.browser.contents)
+        self.assertEqual(self.student['studycourse']['110'].validated_by,
+            'Helen Procter')
+        self.assertMatches(
+            '<YYYY-MM-DD hh:mm:ss>',
+            self.student['studycourse']['110'].validation_date.strftime(
+                "%Y-%m-%d %H:%M:%S"))
+        self.browser.getLink("Reject courses").click()
+        self.assertTrue('Course list request has been annulled.'
+            in self.browser.contents)
+        urlmessage = 'Course+list+request+has+been+annulled.'
+        self.assertEqual(self.browser.url, self.student_path +
+            '/contactstudent?subject=%s' % urlmessage)
+        self.assertTrue('school fee paid' in self.browser.contents)
+        self.assertTrue(self.student['studycourse']['110'].validated_by is None)
+        self.assertTrue(self.student['studycourse']['110'].validation_date is None)
+        IWorkflowInfo(self.student).fireTransition('register_courses')
+        self.browser.open(L110_student_path)
+        self.browser.getLink("Reject courses").click()
+        self.assertTrue('Course list has been unregistered'
+            in self.browser.contents)
+        self.assertTrue('school fee paid' in self.browser.contents)
+        # CA does now see the contact form and can send a message.
+        self.browser.getControl(name="form.subject").value = 'Important subject'
+        self.browser.getControl(name="form.body").value = 'Course list rejected'
+        self.browser.getControl("Send message now").click()
+        self.assertTrue('Your message has been sent' in self.browser.contents)
+        # The CA does now see the Edit button and can edit
+        # current study level.
+        self.browser.open(L110_student_path)
+        self.browser.getLink("Edit").click()
+        self.assertTrue('Edit course list of 100 (Year 1) on 1st probation'
+            in self.browser.contents)
+        # The CA can't validate courses if not in state
+        # courses registered.
+        self.browser.open(L110_student_path + '/validate_courses')
+        self.assertTrue('Student is in the wrong state'
+            in self.browser.contents)
+        # The CA can go to his certificate through the my_roles page ...
+        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
+        self.browser.getLink(
+            "http://localhost/app/faculties/fac1/dep1/certificates/CERT1").click()
+        # ... and view the list of students.
+        self.browser.getLink("Show students").click()
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Show").click()
+        self.assertTrue(self.student_id in self.browser.contents)
+
+    def test_change_current_mode(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.clearance_path)
+        self.assertFalse('Employer' in self.browser.contents)
+        self.browser.open(self.manage_clearance_path)
+        self.assertFalse('Employer' in self.browser.contents)
+        self.browser.open(self.edit_clearance_path)
+        self.assertFalse('Employer' in self.browser.contents)
+        # Now we change the study mode of the certificate and a different
+        # interface is used by clearance views.
+        self.certificate.study_mode = 'pg_ft'
+        # Invariants are not being checked here?!
+        self.certificate.end_level = 100
+        self.browser.open(self.clearance_path)
+        self.assertTrue('Employer' in self.browser.contents)
+        self.browser.open(self.manage_clearance_path)
+        self.assertTrue('Employer' in self.browser.contents)
+        IWorkflowState(self.student).setState('clearance started')
+        self.browser.open(self.edit_clearance_path)
+        self.assertTrue('Employer' in self.browser.contents)
+
+    def test_find_students_in_faculties(self):
+        # Create local students manager in faculty
+        self.app['users'].addUser('mrmanager', SECRET)
+        self.app['users']['mrmanager'].email = 'mrmanager@foo.ng'
+        self.app['users']['mrmanager'].title = u'Volk Wagen'
+        # Assign LocalStudentsManager role for faculty
+        fac = self.app['faculties']['fac1']
+        prmlocal = IPrincipalRoleManager(fac)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.LocalStudentsManager', 'mrmanager')
+        notify(LocalRoleSetEvent(
+            fac, 'waeup.local.LocalStudentsManager', 'mrmanager',
+            granted=True))
+        # Login as manager
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrmanager'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Manager can see his roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<span>Students Manager</span>...',
+            self.browser.contents)
+        # The manager can go to his faculty
+        self.browser.getLink(
+            "http://localhost/app/faculties/fac1").click()
+        # and find students
+        self.browser.getLink("Find students").click()
+        self.browser.getControl("Find student").click()
+        self.assertTrue('Empty search string' in self.browser.contents)
+        self.browser.getControl(name="searchtype").value = ['student_id']
+        self.browser.getControl(name="searchterm").value = self.student_id
+        self.browser.getControl("Find student").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+    def test_activate_deactivate_buttons(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.browser.getLink("Deactivate").click()
+        self.assertTrue(
+            'Student account has been deactivated.' in self.browser.contents)
+        self.assertTrue(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertTrue(self.student.suspended)
+        self.browser.getLink("Activate").click()
+        self.assertTrue(
+            'Student account has been activated.' in self.browser.contents)
+        self.assertFalse(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertFalse(self.student.suspended)
+        # History messages have been added ...
+        self.browser.getLink("History").click()
+        self.assertTrue(
+            'Student account deactivated by Manager<br />' in self.browser.contents)
+        self.assertTrue(
+            'Student account activated by Manager<br />' in self.browser.contents)
+        # ... and actions have been logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - students.browser.StudentDeactivateView - '
+                        'K1000000 - account deactivated' in logcontent)
+        self.assertTrue('zope.mgr - students.browser.StudentActivateView - '
+                        'K1000000 - account activated' in logcontent)
+
+    def test_manage_student_transfer(self):
+        # Add second certificate
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = u'CERT2'
+        self.certificate2.study_mode = 'ug_ft'
+        self.certificate2.start_level = 999
+        self.certificate2.end_level = 999
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add study level to old study course
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 999
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.browser.getLink("Transfer").click()
+        self.browser.getControl(name="form.certificate").value = ['CERT2']
+        self.browser.getControl(name="form.current_session").value = ['2011']
+        self.browser.getControl(name="form.current_level").value = ['200']
+        self.browser.getControl("Transfer").click()
+        self.assertTrue(
+            'Current level does not match certificate levels'
+            in self.browser.contents)
+        self.browser.getControl(name="form.current_level").value = ['999']
+        self.browser.getControl("Transfer").click()
+        self.assertTrue('Successfully transferred' in self.browser.contents)
+        # The catalog has been updated
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(
+            certcode=('CERT2', 'CERT2')))
+        self.assertTrue(results[0] is self.student)
+        results = list(
+            cat.searchResults(
+            current_session=(2011, 2011)))
+        self.assertTrue(results[0] is self.student)
+        # Add study level to new study course
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 999
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+
+        # Edit and add pages are locked for old study courses
+        self.browser.open(self.student_path + '/studycourse/manage')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/start_session')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/start_session')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.student_path + '/studycourse/add')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/add')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/999/manage')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/999/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/999/validate_courses')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/999/validate_courses')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/999/reject_courses')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/999/reject_courses')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/999/add')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/999/add')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/999/edit')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/999/edit')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        # Revert transfer
+        self.browser.open(self.student_path + '/studycourse_1')
+        self.browser.getLink("Reactivate").click()
+        self.browser.getControl("Revert now").click()
+        self.assertTrue('Previous transfer reverted' in self.browser.contents)
+        results = list(
+            cat.searchResults(
+            certcode=('CERT1', 'CERT1')))
+        self.assertTrue(results[0] is self.student)
+        self.assertEqual([i for i in self.student.keys()],
+            [u'accommodation', u'payments', u'studycourse'])
+
+    def test_login_as_student(self):
+        # StudentImpersonators can login as student
+        # Create clearance officer
+        self.app['users'].addUser('mrofficer', SECRET)
+        self.app['users']['mrofficer'].email = 'mrofficer@foo.ng'
+        self.app['users']['mrofficer'].title = 'Harry Actor'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.StudentImpersonator', 'mrofficer')
+        prmglobal.assignRoleToPrincipal('waeup.StudentsManager', 'mrofficer')
+        # Login as student impersonator
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrofficer'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.browser.open(self.student_path)
+        self.browser.getLink("Login as").click()
+        self.browser.getControl("Set password now").click()
+        temp_password = self.browser.getControl(name='form.password').value
+        self.browser.getControl("Login now").click()
+        self.assertMatches(
+            '...You successfully logged in as...', self.browser.contents)
+        # We are logged in as student and can see the 'My Data' tab
+        self.assertMatches(
+            '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
+            self.browser.contents)
+        self.assertMatches(
+            '...My Data...',
+            self.browser.contents)
+        self.browser.getLink("Logout").click()
+        # The student can't login with the original password ...
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...Your account has been temporarily deactivated...',
+            self.browser.contents)
+        # ... but with the temporary password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = temp_password
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Creation of temp_password is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrofficer - students.browser.LoginAsStudentStep1 - K1000000 - '
+            'temp_password generated: %s' % temp_password in logcontent)
+
+    def test_transcripts(self):
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2005
+        self.student['studycourse'].entry_mode = 'ug_ft'
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        studylevel2 = createObject(u'waeup.StudentStudyLevel')
+        studylevel2.level = 110
+        studylevel2.level_session = 2006
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel2)
+        # Add second course (COURSE has been added automatically)
+        courseticket = createObject('waeup.CourseTicket')
+        courseticket.code = 'ANYCODE'
+        courseticket.title = u'Any TITLE'
+        courseticket.credits = 13
+        courseticket.score = 66
+        courseticket.semester = 1
+        courseticket.dcode = u'ANYDCODE'
+        courseticket.fcode = u'ANYFCODE'
+        self.student['studycourse']['110']['COURSE2'] = courseticket
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 3.0)
+        self.assertEqual(self.student['studycourse']['110'].gpa_params_rectified[0], 4.0)
+        # Get transcript data
+        td = self.student['studycourse'].getTranscriptData()
+        self.assertEqual(td[0][0]['level_key'], '100')
+        self.assertEqual(td[0][0]['sgpa'], 3.0)
+        self.assertEqual(td[0][0]['level'].level, 100)
+        self.assertEqual(td[0][0]['level'].level_session, 2005)
+        self.assertEqual(td[0][0]['tickets_1'][0].code, 'COURSE1')
+        self.assertEqual(td[0][1]['level_key'], '110')
+        self.assertEqual(td[0][1]['sgpa'], 4.0)
+        self.assertEqual(td[0][1]['level'].level, 110)
+        self.assertEqual(td[0][1]['level'].level_session, 2006)
+        self.assertEqual(td[0][1]['tickets_1'][0].code, 'ANYCODE')
+        self.assertEqual(td[1], 3.5652173913043477)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path + '/studycourse/transcript')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Transcript' in self.browser.contents)
+        # Officers can open the pdf transcript
+        self.browser.open(self.student_path + '/studycourse/transcript.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'transcript.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF transcript.pdf written to %s" % path
+
+    def test_process_transcript(self):
+        IWorkflowState(self.student).setState('transcript requested')
+        notify(grok.ObjectModifiedEvent(self.student))
+        self.student['studycourse'].transcript_comment = (
+            u'On 07/08/2013 08:59:54 UTC K1000000 wrote:\n\nComment line 1 \n'
+            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
+            'Address line2\n\n')
+        # Create officer with both roles
+        self.app['users'].addUser('mrtranscript', SECRET)
+        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
+        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.TranscriptOfficer', 'mrtranscript')
+        prmglobal.assignRoleToPrincipal('waeup.StudentsManager', 'mrtranscript')
+        prmglobal.assignRoleToPrincipal('waeup.TranscriptSignee', 'mrtranscript')
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrtranscript'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Officer can see his roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Transcript Officer</div>...',
+            self.browser.contents)
+        # Officer can search for students in state 'transcript requested'
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = [
+            'transcript requested']
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.getLink("K1000000").click()
+        self.assertFalse('Release transcript request' in self.browser.contents)
+        # Officers can still edit studycourse, studylevel and course tickets.
+        self.browser.open(self.studycourse_path + '/manage')
+        self.assertTrue('Undergraduate Full-Time</option>'
+            in self.browser.contents)
+        self.browser.getControl(name="form.certificate").value = ['CERT1']
+        self.browser.getControl(name="form.current_session").value = ['2004']
+        self.browser.getControl(name="form.current_verdict").value = ['A']
+        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="form.current_level").value = ['100']
+        self.browser.getControl("Save").click()
+        self.browser.getControl(name="addlevel").value = ['100']
+        self.browser.getControl(name="level_session").value = ['2004']
+        self.browser.getControl("Add study level").click()
+        self.browser.getLink("100").click()
+        self.browser.getLink("Manage").click()
+        self.browser.getControl(name="form.level_session").value = ['2002']
+        self.browser.getControl("Save").click()
+        self.browser.getLink("COURSE1").click()
+        self.browser.getLink("Manage").click()
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        # Officer can edit transcript remarks and validate the transcript
+        self.browser.open(self.studycourse_path + '/transcript')
+        self.browser.getLink("Validate transcript").click()
+        self.browser.getLink("Edit").click()
+        self.assertEqual(
+            self.browser.url, self.studycourse_path + '/100/remark')
+        self.browser.getControl(
+            name="form.transcript_remark").value = 'Oh, the student failed'
+        self.browser.getControl(
+            "Save remark and go and back to transcript validation page").click()
+        self.assertEqual(
+            self.browser.url,
+            self.studycourse_path + '/validate_transcript#tab4')
+        self.assertEqual(self.student['studycourse']['100'].transcript_remark,
+            'Oh, the student failed')
+        self.browser.getControl("Save comment and validate transcript").click()
+        # After validation all manage forms are locked.
+        self.browser.open(self.studycourse_path + '/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.assertFalse('Undergraduate Full-Time</option>'
+            in self.browser.contents)
+        self.browser.open(self.studycourse_path + '/100/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.studycourse_path + '/100/COURSE1/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.studycourse_path + '/100/remark')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        # Transcript can be signed if officer has the permission to sign
+        #self.browser.open(self.studycourse_path + '/transcript')
+        #self.assertFalse('Sign transcript' in self.browser.contents)
+        #prmglobal = IPrincipalRoleManager(self.app)
+        #prmglobal.assignRoleToPrincipal('waeup.TranscriptSignee', 'mrtranscript')
+
+        self.browser.open(self.studycourse_path + '/transcript')
+        self.browser.getLink("Sign transcript electronically").click()
+        # Transcript signing has been logged ...
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrtranscript - students.browser.StudentTranscriptSignView - '
+            'K1000000 - Transcript signed' in logcontent)
+        # ... appears in the student's history ...
+        self.browser.open(self.history_path)
+        self.assertTrue('Transcript signed by Ruth Gordon'
+            in self.browser.contents)
+        # ... and is also stored in the transcript_signee attribute.
+        self.assertTrue(
+            u'Electronically signed by Ruth Gordon (mrtranscript) on '
+            in self.student['studycourse'].transcript_signees)
+        # Officer can release the transcript
+        self.browser.open(self.studycourse_path + '/transcript')
+        self.browser.getLink("Release transcript").click()
+        self.assertTrue(' UTC K1000000 wrote:<br><br>Comment line 1 <br>'
+        'Comment line2<br><br>Dispatch Address:<br>Address line 1 <br>'
+        'Address line2<br><br></p>' in self.browser.contents)
+        self.browser.getControl(name="comment").value = (
+            'Hello,\nYour transcript has been sent to the address provided.')
+        self.browser.getControl("Save comment and release transcript").click()
+        self.assertTrue(
+            'UTC mrtranscript wrote:\n\nHello,\nYour transcript has '
+            'been sent to the address provided.\n\n'
+            in self.student['studycourse'].transcript_comment)
+        # The comment has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrtranscript - students.browser.StudentTranscriptReleaseFormPage - '
+            'K1000000 - comment: Hello,<br>'
+            'Your transcript has been sent to the address provided'
+            in logcontent)
+        # File has been stored in the file system
+        # Check if transcript exists in the file system and is a PDF file
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(
+            self.student).chooseName(attr='final_transcript.pdf')
+        pdf = storage.getFile(file_id).read()
+        self.assertTrue(len(pdf) > 0)
+        self.assertEqual(pdf[:8], '%PDF-1.4')
+        # Copy the file to samples_dir
+        path = os.path.join(samples_dir(), 'final_transcript.pdf')
+        open(path, 'wb').write(pdf)
+        print "Sample PDF final_transcript.pdf written to %s" % path
+        # Check if there is an transcript pdf link in UI
+        self.browser.open(self.student_path)
+        self.assertTrue('Final Transcript' in self.browser.contents)
+        self.browser.getLink("Final Transcript").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        # Transcript views are no longer accessible
+        self.browser.open(self.studycourse_path)
+        self.assertFalse('studycourse/transcript' in self.browser.contents)
+        self.browser.open(self.studycourse_path + '/transcript')
+        self.assertTrue('Forbidden!' in self.browser.contents)
+        self.browser.open(self.studycourse_path + '/transcript.pdf')
+        self.assertTrue('Forbidden!' in self.browser.contents)
+        # If we reset the transcript process
+        # (can't be done by transcript officer), the file will be deleted
+        IWorkflowInfo(self.student).fireTransition('reset11')
+        self.browser.open(self.student_path)
+        self.assertFalse('Final Transcript' in self.browser.contents)
+        # ... and transcript process information has been removed
+        self.assertEqual(self.student['studycourse'].transcript_comment, None)
+        self.assertEqual(self.student['studycourse'].transcript_signees, None)
+
+    def test_landingpage_transcript_officer(self):
+        IWorkflowState(self.student).setState('transcript requested')
+        notify(grok.ObjectModifiedEvent(self.student))
+        # Create transcript officer
+        self.app['users'].addUser('mrtranscript', SECRET)
+        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
+        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
+        # We assign transcript officer role at faculty level
+        fac = self.app['faculties']['fac1']
+        prmlocal = IPrincipalRoleManager(fac)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.TranscriptOfficer', 'mrtranscript')
+        notify(LocalRoleSetEvent(
+            fac, 'waeup.local.TranscriptOfficer', 'mrtranscript', granted=True))
+        # Login as transcript officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrtranscript'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Officer is on landing page and does see the transcript link
+        self.assertTrue(
+            'http://localhost/app/students/K1000000/studycourse/transcript'
+            in self.browser.contents)
+        self.browser.getLink("K1000000").click()
+        self.assertTrue(
+            'Anna Tester: Transcript Data' in self.browser.contents)
+        # Officer is on transcript page and can validate the transcript
+        self.browser.getLink("Validate transcript").click()
+        self.browser.getControl("Save comment and validate transcript").click()
+        self.assertTrue(
+            '<div class="alert alert-success">Transcript validated.</div>'
+            in self.browser.contents)
+        # Officer is still on transcript page and can release the transcript
+        self.browser.getLink("Release transcript").click()
+        self.browser.getControl("Save comment and release transcript").click()
+        self.assertTrue(
+            '<div class="alert alert-success">'
+            'Transcript released and final transcript file saved.</div>'
+            in self.browser.contents)
+
+    def test_landingpage_transcript_signee(self):
+        IWorkflowState(self.student).setState('transcript validated')
+        notify(grok.ObjectModifiedEvent(self.student))
+        # Create transcript signee
+        self.app['users'].addUser('mrtranscript', SECRET)
+        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
+        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
+        # We assign transcript officer role at faculty level
+        fac = self.app['faculties']['fac1']
+        prmlocal = IPrincipalRoleManager(fac)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.TranscriptSignee', 'mrtranscript')
+        notify(LocalRoleSetEvent(
+            fac, 'waeup.local.TranscriptSignee', 'mrtranscript', granted=True))
+        # Login as transcript officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrtranscript'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # Officer is on landing page and does see the transcript link
+        self.assertTrue(
+            'http://localhost/app/students/K1000000/studycourse/transcript'
+            in self.browser.contents)
+        self.browser.getLink("K1000000").click()
+        self.assertTrue(
+            'Anna Tester: Transcript Data' in self.browser.contents)
+        # Officer is on transcript page and can sign the transcript
+        self.browser.getLink("Sign transcript").click()
+        self.assertTrue(
+            '<div class="alert alert-success">Transcript signed.</div>'
+            in self.browser.contents)
+        # Officer is still on transcript page
+        self.assertTrue(
+            'Anna Tester: Transcript Data' in self.browser.contents)
+        # Officer can sign the transcript only once
+        self.browser.getLink("Sign transcript").click()
+        self.assertTrue(
+            '<div class="alert alert-warning">'
+            'You have already signed this transcript.</div>'
+            in self.browser.contents)
+        # Signature can be seen on transcript page
+        self.assertTrue(
+            'Electronically signed by Ruth Gordon (mrtranscript) on'
+            in self.browser.contents)
+
+    def test_update_coursetickets(self):
+        IWorkflowState(self.student).setState('school fee paid')
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2015
+        self.student['studycourse'].entry_mode = 'ug_ft'
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(
+            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1/')
+        self.assertFalse(
+            'Update session 2015/2016 credits' in self.browser.contents)
+        self.app['configuration'].current_academic_session = 2015
+        self.browser.open(
+            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1/')
+        self.browser.getLink("Update session 2015/2016 credits").click()
+        self.assertTrue(
+            'No course ticket found.' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - browser.pages.UpdateCourseTicketsView - '
+            'course tickets updated: COURSE1' in logcontent)
+        studylevel['COURSE1'].credits = 12
+        self.browser.getLink("Update session 2015/2016 credits").click()
+        self.assertTrue(
+            'No course ticket found.' in self.browser.contents)
+        studylevel.level_session = 2015
+        self.student['studycourse'].current_session = 2015
+        self.browser.getLink("Update session 2015/2016 credits").click()
+        self.assertTrue(
+            '1 course ticket(s) updated.' in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.utils.StudentsUtils - '
+            'K1000000 100/COURSE1 credits updated (10->12)' in logcontent)
+        return
+
+
+class StudentUITests(StudentsFullSetup):
+    # Tests for Student class views and pages
+
+    def test_student_change_password(self):
+        # Students can change the password
+        self.student.personal_updated = datetime.utcnow()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.student_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        # Change password
+        self.browser.getLink("Change password").click()
+        self.browser.getControl(name="change_password").value = 'pw'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'pw'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password must have at least' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_passssword'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Passwords do not match' in self.browser.contents)
+        self.browser.getControl(name="change_password").value = 'new_password'
+        self.browser.getControl(
+            name="change_password_repeat").value = 'new_password'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Password changed' in self.browser.contents)
+        # We are still logged in. Changing the password hasn't thrown us out.
+        self.browser.getLink("Base Data").click()
+        self.assertEqual(self.browser.url, self.student_path)
+        # We can logout
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        self.assertEqual(self.browser.url, 'http://localhost/app/index')
+        # We can login again with the new password
+        self.browser.getLink("Login").click()
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'new_password'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.student_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        return
+
+    def test_forbidden_name(self):
+        self.student.lastname = u'<TAG>Tester</TAG>'
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue('XXX: Base Data' in self.browser.contents)
+        self.assertTrue('&lt;TAG&gt;Tester&lt;/TAG&gt;' in self.browser.contents)
+        self.assertFalse('<TAG>Tester</TAG>' in self.browser.contents)
+        return
+
+    def test_setpassword(self):
+        # Set password for first-time access
+        student = Student()
+        student.reg_number = u'123456'
+        student.firstname = u'Klaus'
+        student.lastname = u'Tester'
+        self.app['students'].addStudent(student)
+        setpassword_path = 'http://localhost/app/setpassword'
+        student_path = 'http://localhost/app/students/%s' % student.student_id
+        self.browser.open(setpassword_path)
+        self.browser.getControl(name="ac_series").value = self.existing_pwdseries
+        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
+        self.browser.getControl(name="reg_number").value = '223456'
+        self.browser.getControl("Set").click()
+        self.assertMatches('...No student found...',
+                           self.browser.contents)
+        self.browser.getControl(name="reg_number").value = '123456'
+        self.browser.getControl(name="ac_number").value = '999999'
+        self.browser.getControl("Set").click()
+        self.assertMatches('...Access code is invalid...',
+                           self.browser.contents)
+        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
+        self.browser.getControl("Set").click()
+        self.assertMatches('...Password has been set. Your Student Id is...',
+                           self.browser.contents)
+        self.browser.getControl("Set").click()
+        self.assertMatches(
+            '...Password has already been set. Your Student Id is...',
+            self.browser.contents)
+        existing_pwdpin = self.pwdpins[1]
+        parts = existing_pwdpin.split('-')[1:]
+        existing_pwdseries, existing_pwdnumber = parts
+        self.browser.getControl(name="ac_series").value = existing_pwdseries
+        self.browser.getControl(name="ac_number").value = existing_pwdnumber
+        self.browser.getControl(name="reg_number").value = '123456'
+        self.browser.getControl("Set").click()
+        self.assertMatches(
+            '...You are using the wrong Access Code...',
+            self.browser.contents)
+        # The student can login with the new credentials
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = student.student_id
+        self.browser.getControl(
+            name="form.password").value = self.existing_pwdnumber
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, student_path)
+        self.assertTrue('You logged in' in self.browser.contents)
+        return
+
+    def test_student_login(self):
+        # Student cant login if their password is not set
+        self.student.password = None
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You entered invalid credentials.' in self.browser.contents)
+        # We set the password again
+        IUserAccount(
+            self.app['students'][self.student_id]).setPassword('spwd')
+        # Students can't login if their account is suspended/deactivated
+        self.student.suspended = True
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">'
+            'Your account has been deactivated.</div>...', self.browser.contents)
+        # If suspended_comment is set this message will be flashed instead
+        self.student.suspended_comment = u'Aetsch baetsch!'
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...<div class="alert alert-warning">Aetsch baetsch!</div>...',
+            self.browser.contents)
+        self.student.suspended = False
+        # Students can't login if a temporary password has been set and
+        # is not expired
+        self.app['students'][self.student_id].setTempPassword(
+            'anybody', 'temp_spwd')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...Your account has been temporarily deactivated...',
+            self.browser.contents)
+        # The student can login with the temporary password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'temp_spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Student can view the base data
+        self.browser.open(self.student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.student_path)
+        # When the password expires ...
+        delta = timedelta(minutes=11)
+        self.app['students'][self.student_id].temp_password[
+            'timestamp'] = datetime.utcnow() - delta
+        self.app['students'][self.student_id]._p_changed = True
+        # ... the student will be automatically logged out
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.student_path)
+        # Then the student can login with the original password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+
+    def test_maintenance_mode(self):
+        config = grok.getSite()['configuration']
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Student logged in.
+        self.assertTrue('You logged in' in self.browser.contents)
+        self.assertTrue("Anna Tester" in self.browser.contents)
+        # If maintenance mode is enabled, student is immediately logged out.
+        config.maintmode_enabled_by = u'any_user'
+        self.assertRaises(
+            Unauthorized, self.browser.open, 'http://localhost/app/faculties')
+        self.browser.open('http://localhost/app/login')
+        self.assertTrue('The portal is in maintenance mode' in self.browser.contents)
+        # Student really can't login if maintenance mode is enabled.
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # A second warning is raised.
+        self.assertTrue(
+            'The portal is in maintenance mode. You can\'t login!'
+            in self.browser.contents)
+        return
+
+    def test_student_clearance(self):
+        # Student cant login if their password is not set
+        IWorkflowInfo(self.student).fireTransition('admit')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Admitted student can upload a passport picture
+        self.browser.open(self.student_path + '/change_portrait')
+        ctrl = self.browser.getControl(name='passportuploadedit')
+        file_obj = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passportuploadedit').click()
+        self.assertTrue(
+            'src="http://localhost/app/students/K1000000/passport.jpg"'
+            in self.browser.contents)
+        # Students can open admission letter
+        self.browser.getLink("Base Data").click()
+        self.browser.getLink("Download admission letter").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'admission_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF admission_slip.pdf written to %s" % path
+        # Student can view the clearance data
+        self.browser.open(self.student_path)
+        self.browser.getLink("Clearance Data").click()
+        # Student can't open clearance edit form before starting clearance
+        self.browser.open(self.student_path + '/cedit')
+        self.assertMatches('...The requested form is locked...',
+                           self.browser.contents)
+        self.browser.getLink("Clearance Data").click()
+        self.browser.getLink("Start clearance").click()
+        self.student.phone = None
+        # Uups, we forgot to fill the phone fields
+        self.browser.getControl("Start clearance").click()
+        self.assertMatches('...Phone number is missing...',
+                           self.browser.contents)
+        self.browser.open(self.student_path + '/edit_base')
+        self.browser.getControl(name="form.phone.ext").value = '12345'
+        self.browser.getControl("Save").click()
+        self.browser.open(self.student_path + '/start_clearance')
+        self.browser.getControl(name="ac_series").value = '3'
+        self.browser.getControl(name="ac_number").value = '4444444'
+        self.browser.getControl("Start clearance now").click()
+        self.assertMatches('...Activation code is invalid...',
+                           self.browser.contents)
+        self.browser.getControl(name="ac_series").value = self.existing_clrseries
+        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
+        # Owner is Hans Wurst, AC can't be invalidated
+        self.browser.getControl("Start clearance now").click()
+        self.assertMatches('...You are not the owner of this access code...',
+                           self.browser.contents)
+        # Set the correct owner
+        self.existing_clrac.owner = self.student_id
+        # clr_code might be set (and thus returns None) due importing
+        # an empty clr_code column.
+        self.student.clr_code = None
+        self.browser.getControl("Start clearance now").click()
+        self.assertMatches('...Clearance process has been started...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save", index=0).click()
+        # Student can view the clearance data
+        self.browser.getLink("Clearance Data").click()
+        # and go back to the edit form
+        self.browser.getLink("Edit").click()
+        # Students can upload documents
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_obj = open(SAMPLE_IMAGE, 'rb')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file_obj, filename='my_birth_certificate.jpg')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue(
+            'href="http://localhost/app/students/K1000000/birth_certificate"'
+            in self.browser.contents)
+        # Students can open clearance slip
+        self.browser.getLink("View").click()
+        self.browser.getLink("Download clearance slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        # Students can request clearance
+        self.browser.open(self.edit_clearance_path)
+        self.browser.getControl("Save and request clearance").click()
+        self.browser.getControl(name="ac_series").value = self.existing_clrseries
+        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
+        self.browser.getControl("Request clearance now").click()
+        self.assertMatches('...Clearance has been requested...',
+                           self.browser.contents)
+        # Student can't reopen clearance form after requesting clearance
+        self.browser.open(self.student_path + '/cedit')
+        self.assertMatches('...The requested form is locked...',
+                           self.browser.contents)
+
+    def test_student_course_registration(self):
+        # Student cant login if their password is not set
+        IWorkflowInfo(self.student).fireTransition('admit')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Student can't add study level if not in state 'school fee paid'
+        self.browser.open(self.student_path + '/studycourse/add')
+        self.assertMatches('...The requested form is locked...',
+                           self.browser.contents)
+        # ... and must be transferred first
+        IWorkflowState(self.student).setState('school fee paid')
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.student['studycourse'].current_level = None
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Your data are incomplete...',
+                           self.browser.contents)
+        self.student['studycourse'].current_level = 100
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        # A level with one course ticket was created
+        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
+        self.browser.getLink("100").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches('...The ticket exists...',
+                           self.browser.contents)
+        self.student['studycourse'].current_level = 200
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 200 (Year 2)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        self.browser.getLink("200").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.course.credits = 100
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches(
+            '...Maximum credits exceeded...', self.browser.contents)
+        self.course.credits = 10
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches('...The ticket exists...',
+                           self.browser.contents)
+        # Indeed the ticket exists as carry-over course from level 100
+        # since its score was 0
+        self.assertTrue(
+            self.student['studycourse']['200']['COURSE1'].carry_over is True)
+        self.assertTrue(
+            self.student['studycourse']['200']['COURSE1'].course_category is None)
+        # Students can open the pdf course registration slip
+        self.browser.open(self.student_path + '/studycourse/200')
+        self.browser.getLink("Download course registration slip").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'course_registration_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF course_registration_slip.pdf written to %s" % path
+        # Students can remove course tickets
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('No ticket selected' in self.browser.contents)
+        # No ticket can be selected since the carry-over course is a core course
+        self.assertRaises(
+            LookupError, self.browser.getControl, name='val_id')
+        self.student['studycourse']['200']['COURSE1'].mandatory = False
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        # Course list can't be registered if total_credits exceeds max_credits
+        self.student['studycourse']['200']['COURSE1'].credits = 60
+        self.browser.getControl("Register course list").click()
+        self.assertTrue('Maximum credits exceeded' in self.browser.contents)
+        # Student can now remove the ticket
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='COURSE1').selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+        # Removing course tickets is properly logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage '
+        '- K1000000 - level 200 - removed: COURSE1' in logcontent)
+        # They can add the same ticket using the edit page directly.
+        # We can do the same by adding the course on the manage page directly
+        self.browser.getControl(name="course").value = 'COURSE1'
+        self.browser.getControl("Add course ticket").click()
+        # Adding course tickets is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage - '
+            'K1000000 - level 200 - added: COURSE1|200|2004' in logcontent)
+        # Course list can be registered
+        self.browser.getControl("Register course list").click()
+        self.assertTrue('Course list has been registered' in self.browser.contents)
+        self.assertEqual(self.student.state, 'courses registered')
+        # Course list can be unregistered
+        self.browser.getLink("Unregister courses").click()
+        self.assertEqual(self.student.state, 'school fee paid')
+        self.assertTrue('Course list has been unregistered' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse/200/unregister_courses')
+        self.assertTrue('You are in the wrong state' in self.browser.contents)
+        # Students can view the transcript
+        #self.browser.open(self.studycourse_path)
+        #self.browser.getLink("Transcript").click()
+        #self.browser.getLink("Academic Transcript").click()
+        #self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        #self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        return
+
+    def test_student_ticket_update(self):
+        IWorkflowState(self.student).setState('school fee paid')
+        self.student['studycourse'].current_level = 100
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        # A level with one course ticket was created
+        self.assertEqual(
+            self.student['studycourse']['100'].number_of_tickets, 1)
+        self.browser.getLink("100").click()
+        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
+        self.browser.getLink("Edit course list").click()
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue('All course tickets updated.' in self.browser.contents)
+        # ... nothing has changed
+        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
+        # We change the title of the course
+        self.course.title = u'New Title'
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue('<td>New Title</td>' in self.browser.contents)
+        # We remove the course
+        del self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        self.browser.getControl("Update all tickets").click()
+        self.assertTrue(' <td>New Title (course cancelled)</td>'
+            in self.browser.contents)
+        # Course ticket invalidation has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'K1000000 - students.browser.StudyLevelEditFormPage - '
+            'K1000000 - level 100 - course tickets invalidated: COURSE1'
+            in logcontent)
+        return
+
+    def test_student_course_already_passed(self):
+        IWorkflowState(self.student).setState('school fee paid')
+        self.student['studycourse'].current_level = 100
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        # A level with one course ticket was created
+        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
+        # We set the score above the passmark
+        self.student['studycourse']['100'][
+            'COURSE1'].score = self.student['studycourse']['100'][
+                'COURSE1'].passmark + 1
+        # We add a second level
+        self.student['studycourse'].current_level = 200
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 200 (Year 2)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        self.browser.getLink("200").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue(
+            'Course has already been passed at previous level'
+            in self.browser.contents)
+        self.assertEqual(self.student['studycourse']['200'].number_of_tickets, 0)
+        # We set the score below the passmark
+        self.student['studycourse']['100'][
+            'COURSE1'].score = self.student['studycourse']['100'][
+                'COURSE1'].passmark - 1
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue(
+            'Successfully added COURSE1' in self.browser.contents)
+        self.assertEqual(self.student['studycourse']['200'].number_of_tickets, 1)
+        return
+
+    def test_student_course_registration_outstanding(self):
+        self.course = createObject('waeup.Course')
+        self.course.code = 'COURSE2'
+        self.course.semester = 1
+        self.course.credits = 45
+        self.course.passmark = 40
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(
+            self.course)
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.browser.open(self.student_path + '/studycourse/add')
+        self.browser.getControl("Create course list now").click()
+        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
+        self.student['studycourse'].current_level = 200
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 200 (Year 2)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        self.browser.getLink("200").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE2']
+        self.browser.getControl("Add course ticket").click()
+        # Carryover COURSE1 in level 200 already has 10 credits
+        self.assertMatches(
+            '...Maximum credits exceeded...', self.browser.contents)
+        # If COURSE1 is outstanding, its credits won't be considered
+        self.student['studycourse']['200']['COURSE1'].outstanding = True
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches(
+            '...Successfully added COURSE2...', self.browser.contents)
+        return
+
+    def test_postgraduate_student_access(self):
+        self.certificate.study_mode = 'pg_ft'
+        self.certificate.start_level = 999
+        self.certificate.end_level = 999
+        self.student['studycourse'].current_level = 999
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level Postgraduate Level...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        self.assertTrue("You successfully created a new course list"
+            in self.browser.contents)
+        # A level with one course ticket was created
+        self.assertEqual(self.student['studycourse']['999'].number_of_tickets, 0)
+        self.browser.getLink("Edit course list").click()
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertMatches('...Successfully added COURSE1...',
+                           self.browser.contents)
+        # Postgraduate students can't register course lists
+        self.browser.getControl("Register course list").click()
+        self.assertTrue("your course list can't bee registered"
+            in self.browser.contents)
+        self.assertEqual(self.student.state, 'school fee paid')
+        return
+
+    def test_student_clearance_wo_clrcode(self):
+        IWorkflowState(self.student).setState('clearance started')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.browser.open(self.edit_clearance_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save and request clearance").click()
+        self.assertMatches('...Clearance has been requested...',
+                           self.browser.contents)
+
+    def test_student_clearance_payment(self):
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+
+        # Students can add online clearance payment tickets
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['clearance']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+
+        # Students can't approve the payment
+        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        payment_url = self.browser.url
+        self.assertRaises(
+            Unauthorized, self.browser.open, payment_url + '/approve')
+        # In the base package they can 'use' a fake approval view.
+        # XXX: I tried to use
+        # self.student['payments'][value].approveStudentPayment() instead.
+        # But this function fails in
+        # w.k.accesscodes.accesscode.create_accesscode.
+        # grok.getSite returns None in tests.
+        self.browser.open(payment_url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+        expected = '''...
+        <td>
+          <span>Paid</span>
+        </td>...'''
+        expected = '''...
+        <td>
+          <span>Paid</span>
+        </td>...'''
+        self.assertMatches(expected,self.browser.contents)
+        payment_id = self.student['payments'].keys()[0]
+        payment = self.student['payments'][payment_id]
+        self.assertEqual(payment.p_state, 'paid')
+        self.assertEqual(payment.r_amount_approved, 3456.0)
+        self.assertEqual(payment.r_code, 'AP')
+        self.assertEqual(payment.r_desc, u'Payment approved by Anna Tester')
+        # The new CLR-0 pin has been created
+        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
+        pin = self.app['accesscodes']['CLR-0'].keys()[0]
+        ac = self.app['accesscodes']['CLR-0'][pin]
+        self.assertEqual(ac.owner, self.student_id)
+        self.assertEqual(ac.cost, 3456.0)
+
+        # Students can open the pdf payment slip
+        self.browser.open(payment_url + '/payment_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+
+        # The new CLR-0 pin can be used for starting clearance
+        # but they have to upload a passport picture first
+        # which is only possible in state admitted
+        self.browser.open(self.student_path + '/change_portrait')
+        self.assertMatches('...form is locked...',
+                          self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('admit')
+        self.browser.open(self.student_path + '/change_portrait')
+        image = open(SAMPLE_IMAGE, 'rb')
+        ctrl = self.browser.getControl(name='passportuploadedit')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(image, filename='my_photo.jpg')
+        self.browser.getControl(
+            name='upload_passportuploadedit').click()
+        self.browser.open(self.student_path + '/start_clearance')
+        parts = pin.split('-')[1:]
+        clrseries, clrnumber = parts
+        self.browser.getControl(name="ac_series").value = clrseries
+        self.browser.getControl(name="ac_number").value = clrnumber
+        self.browser.getControl("Start clearance now").click()
+        self.assertMatches('...Clearance process has been started...',
+                           self.browser.contents)
+
+    def test_student_schoolfee_payment(self):
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2005
+        self.app['configuration'].addSessionConfiguration(configuration)
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+
+        # Students can add online school fee payment tickets.
+        IWorkflowState(self.student).setState('returning')
+        self.browser.open(self.payments_path)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name='val_id')
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
+        # Payment session and will be calculated as defined
+        # in w.k.students.utils because we set changed the state
+        # to returning
+        self.assertEqual(self.student['payments'][value].p_session, 2005)
+        self.assertEqual(self.student['payments'][value].p_level, 200)
+
+        # Student is the payer of the payment ticket.
+        payer = IPayer(self.student['payments'][value])
+        self.assertEqual(payer.display_fullname, 'Anna Tester')
+        self.assertEqual(payer.id, self.student_id)
+        self.assertEqual(payer.faculty, 'fac1')
+        self.assertEqual(payer.department, 'dep1')
+
+        # We simulate the approval
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
+        self.browser.open(self.browser.url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+
+        ## The new SFE-0 pin can be used for starting new session
+        #self.browser.open(self.studycourse_path)
+        #self.browser.getLink('Start new session').click()
+        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
+        #parts = pin.split('-')[1:]
+        #sfeseries, sfenumber = parts
+        #self.browser.getControl(name="ac_series").value = sfeseries
+        #self.browser.getControl(name="ac_number").value = sfenumber
+        #self.browser.getControl("Start now").click()
+        #self.assertMatches('...Session started...',
+        #                   self.browser.contents)
+
+        self.assertTrue(self.student.state == 'school fee paid')
+        return
+
+    def test_student_bedallocation_payment(self):
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.browser.open(self.payments_path)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        # Students can remove only online payment tickets which have
+        # not received a valid callback
+        self.browser.open(self.payments_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        ctrl.getControl(value=value).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertTrue('Successfully removed' in self.browser.contents)
+
+    def test_student_maintenance_payment(self):
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.browser.open(self.payments_path)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...You have not yet booked accommodation...',
+                           self.browser.contents)
+        # We continue this test in test_student_accommodation
+
+    def test_student_previous_payments(self):
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2000
+        configuration.clearance_fee = 3456.0
+        configuration.booking_fee = 123.4
+        self.app['configuration'].addSessionConfiguration(configuration)
+        configuration2 = createObject('waeup.SessionConfiguration')
+        configuration2.academic_session = 2003
+        configuration2.clearance_fee = 3456.0
+        configuration2.booking_fee = 123.4
+        self.app['configuration'].addSessionConfiguration(configuration2)
+        configuration3 = createObject('waeup.SessionConfiguration')
+        configuration3.academic_session = 2005
+        configuration3.clearance_fee = 3456.0
+        configuration3.booking_fee = 123.4
+        self.app['configuration'].addSessionConfiguration(configuration3)
+        self.student['studycourse'].entry_session = 2002
+
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+
+        # Students can add previous school fee payment tickets in any state.
+        IWorkflowState(self.student).setState('courses registered')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add previous session payment ticket").click()
+
+        # Previous session payment form is provided
+        self.assertEqual(self.student.current_session, 2004)
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl(name="form.p_session").value = ['2000']
+        self.browser.getControl(name="form.p_level").value = ['300']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...The previous session must not fall below...',
+                           self.browser.contents)
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl(name="form.p_session").value = ['2005']
+        self.browser.getControl(name="form.p_level").value = ['300']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...This is not a previous session...',
+                           self.browser.contents)
+
+        # Students can pay current session school fee.
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl(name="form.p_session").value = ['2004']
+        self.browser.getControl(name="form.p_level").value = ['300']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
+
+        # Ticket creation is logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'K1000000 - students.browser.PreviousPaymentAddFormPage - '
+            'K1000000 - added: %s' % value
+            in logcontent)
+
+        # Payment session is properly set
+        self.assertEqual(self.student['payments'][value].p_session, 2004)
+        self.assertEqual(self.student['payments'][value].p_level, 300)
+
+        # We simulate the approval
+        self.browser.open(self.browser.url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+
+        # No AC has been created
+        self.assertEqual(len(self.app['accesscodes']['SFE-0'].keys()), 0)
+        self.assertTrue(self.student['payments'][value].ac is None)
+
+        # Current payment flag is set False
+        self.assertFalse(self.student['payments'][value].p_current)
+
+        # Button and form are not available for students who are in
+        # states up to cleared
+        self.student['studycourse'].entry_session = 2004
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.open(self.payments_path)
+        self.assertFalse(
+            "Add previous session payment ticket" in self.browser.contents)
+        self.browser.open(self.payments_path + '/addpp')
+        self.assertTrue(
+            "No previous payment to be made" in self.browser.contents)
+        return
+
+    def test_postgraduate_student_payments(self):
+        configuration = createObject('waeup.SessionConfiguration')
+        configuration.academic_session = 2005
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.certificate.study_mode = 'pg_ft'
+        self.certificate.start_level = 999
+        self.certificate.end_level = 999
+        self.student['studycourse'].current_level = 999
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Students can add online school fee payment tickets.
+        IWorkflowState(self.student).setState('cleared')
+        self.browser.open(self.payments_path)
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        # Payment session and level are current ones.
+        # Postgrads have to pay school_fee_1.
+        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
+        self.assertEqual(self.student['payments'][value].p_session, 2004)
+        self.assertEqual(self.student['payments'][value].p_level, 999)
+
+        # We simulate the approval
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
+        self.browser.open(self.browser.url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+
+        ## The new SFE-0 pin can be used for starting session
+        #self.browser.open(self.studycourse_path)
+        #self.browser.getLink('Start new session').click()
+        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
+        #parts = pin.split('-')[1:]
+        #sfeseries, sfenumber = parts
+        #self.browser.getControl(name="ac_series").value = sfeseries
+        #self.browser.getControl(name="ac_number").value = sfenumber
+        #self.browser.getControl("Start now").click()
+        #self.assertMatches('...Session started...',
+        #                   self.browser.contents)
+
+        self.assertTrue(self.student.state == 'school fee paid')
+
+        # Postgrad students do not need to register courses the
+        # can just pay for the next session.
+        self.browser.open(self.payments_path)
+        # Remove first payment to be sure that we access the right ticket
+        del self.student['payments'][value]
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        # Payment session has increased by one, payment level remains the same.
+        # Returning Postgraduates have to pay school_fee_2.
+        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
+        self.assertEqual(self.student['payments'][value].p_session, 2005)
+        self.assertEqual(self.student['payments'][value].p_level, 999)
+
+        # Student is still in old session
+        self.assertEqual(self.student.current_session, 2004)
+
+        # We do not need to pay the ticket if any other
+        # SFE pin is provided
+        pin_container = self.app['accesscodes']
+        pin_container.createBatch(
+            datetime.utcnow(), 'some_userid', 'SFE', 9.99, 1)
+        pin = pin_container['SFE-1'].values()[0].representation
+        sfeseries, sfenumber = pin.split('-')[1:]
+        # The new SFE-1 pin can be used for starting new session
+        self.browser.open(self.studycourse_path)
+        self.browser.getLink('Start new session').click()
+        self.browser.getControl(name="ac_series").value = sfeseries
+        self.browser.getControl(name="ac_number").value = sfenumber
+        self.browser.getControl("Start now").click()
+        self.assertMatches('...Session started...',
+                           self.browser.contents)
+        self.assertTrue(self.student.state == 'school fee paid')
+        # Student is in new session
+        self.assertEqual(self.student.current_session, 2005)
+        self.assertEqual(self.student['studycourse'].current_level, 999)
+        return
+
+    def test_student_accommodation(self):
+        # Create a second hostel with one bed
+        hostel = Hostel()
+        hostel.hostel_id = u'hall-2'
+        hostel.hostel_name = u'Hall 2'
+        self.app['hostels'].addHostel(hostel)
+        bed = Bed()
+        bed.bed_id = u'hall-2_A_101_A'
+        bed.bed_number = 1
+        bed.owner = NOT_OCCUPIED
+        bed.bed_type = u'regular_female_fr'
+        self.app['hostels'][hostel.hostel_id].addBed(bed)
+        self.app['hostels'].allocation_expiration = 7
+
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Students can add online booking fee payment tickets and open the
+        # callback view (see test_manage_payments).
+        self.browser.getLink("Payments").click()
+        self.browser.getLink("Add current session payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.browser.open(self.browser.url + '/fake_approve')
+        # The new HOS-0 pin has been created.
+        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
+        pin = self.app['accesscodes']['HOS-0'].keys()[0]
+        ac = self.app['accesscodes']['HOS-0'][pin]
+        parts = pin.split('-')[1:]
+        sfeseries, sfenumber = parts
+        # Students can use HOS code and book a bed space with it ...
+        self.browser.open(self.acco_path)
+        # ... but not if booking period has expired ...
+        self.app['hostels'].enddate = datetime.now(pytz.utc)
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...Outside booking period: ...',
+                           self.browser.contents)
+        self.app['hostels'].enddate = datetime.now(pytz.utc) + timedelta(days=10)
+        # ... or student data are incomplete ...
+        self.student['studycourse'].current_level = None
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...Your data are incomplete...',
+            self.browser.contents)
+        self.student['studycourse'].current_level = 100
+        # ... or student is not the an allowed state ...
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...You are in the wrong...',
+                           self.browser.contents)
+        # Students can still not see the disired hostel selector.
+        self.assertFalse('desired hostel' in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('admit')
+        # Students can now see the disired hostel selector.
+        self.browser.reload()
+        self.browser.open(self.acco_path)
+        self.assertTrue('desired hostel' in self.browser.contents)
+        self.browser.getControl(name="hostel").value = ['hall-2']
+        self.browser.getControl("Save").click()
+        self.assertTrue('selection has been saved' in self.browser.contents)
+        self.assertTrue('<option selected="selected" value="hall-2">'
+            in self.browser.contents)
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...Activation Code:...',
+                           self.browser.contents)
+        # Student can't use faked ACs ...
+        self.browser.getControl(name="ac_series").value = u'nonsense'
+        self.browser.getControl(name="ac_number").value = sfenumber
+        self.browser.getControl("Create bed ticket").click()
+        self.assertMatches('...Activation code is invalid...',
+                           self.browser.contents)
+        # ... or ACs owned by somebody else.
+        ac.owner = u'Anybody'
+        self.browser.getControl(name="ac_series").value = sfeseries
+        self.browser.getControl(name="ac_number").value = sfenumber
+        # There is no free bed space and the bed selector does not appear
+        self.assertFalse('<option value="hall-1_A_101_A">'
+            in self.browser.contents)
+        self.browser.getControl("Create bed ticket").click()
+        # Hostel 2 has only a bed for women.
+        self.assertTrue('There is no free bed in your category regular_male_fr.'
+            in self.browser.contents)
+        self.browser.getControl(name="hostel").value = ['hall-1']
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Book accommodation").click()
+        # Student can't use faked ACs ...
+        self.browser.getControl(name="ac_series").value = sfeseries
+        self.browser.getControl(name="ac_number").value = sfenumber
+        self.browser.getControl("Create bed ticket").click()
+        self.assertMatches('...You are not the owner of this access code...',
+                           self.browser.contents)
+        # The bed remains empty.
+        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        self.assertTrue(bed.owner == NOT_OCCUPIED)
+        ac.owner = self.student_id
+        self.browser.open(self.acco_path + '/add')
+        self.browser.getControl(name="ac_series").value = sfeseries
+        self.browser.getControl(name="ac_number").value = sfenumber
+        # Bed can be selected
+        self.browser.getControl(name="bed").value = ['hall-1_A_101_A']
+        self.browser.getControl("Create bed ticket").click()
+        self.assertTrue('Bed ticket created and bed booked'
+            in self.browser.contents)
+        # Bed has been allocated.
+        self.assertTrue(bed.owner == self.student_id)
+        # BedTicketAddPage is now blocked.
+        self.browser.getControl("Book accommodation").click()
+        self.assertMatches('...You already booked a bed space...',
+            self.browser.contents)
+        # The bed ticket displays the data correctly.
+        self.browser.open(self.acco_path + '/2004')
+        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
+                           self.browser.contents)
+        self.assertMatches('...2004/2005...', self.browser.contents)
+        self.assertMatches('...regular_male_fr...', self.browser.contents)
+        self.assertMatches('...%s...' % pin, self.browser.contents)
+        # Students can open the pdf slip.
+        self.browser.open(self.browser.url + '/bed_allocation_slip.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'bed_allocation_slip.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF bed_allocation_slip.pdf written to %s" % path
+        # Students can't relocate themselves.
+        self.assertFalse('Relocate' in self.browser.contents)
+        relocate_path = self.acco_path + '/2004/relocate'
+        self.assertRaises(
+            Unauthorized, self.browser.open, relocate_path)
+        # Students can't see the Remove button and check boxes.
+        self.browser.open(self.acco_path)
+        self.assertFalse('Remove' in self.browser.contents)
+        self.assertFalse('val_id' in self.browser.contents)
+        # Students can pay maintenance fee now.
+        self.browser.open(self.payments_path)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        # Maintennace fee is taken from the hostel object.
+        self.assertEqual(self.student['payments'][value].amount_auth, 876.0)
+        # If the hostel's maintenance fee isn't set, the fee is
+        # taken from the session configuration object.
+        self.app['hostels']['hall-1'].maint_fee = 0.0
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[1]
+        self.assertEqual(self.student['payments'][value].amount_auth, 987.0)
+        # The bedticket is aware of successfull maintenance fee payment
+        bedticket = self.student['accommodation']['2004']
+        self.assertFalse(bedticket.maint_payment_made)
+        self.student['payments'][value].approve()
+        self.assertTrue(bedticket.maint_payment_made)
+        return
+
+    def test_change_password_request(self):
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.identifier").value = '123'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('An email with' in self.browser.contents)
+
+    def test_student_expired_personal_data(self):
+        # Login
+        IWorkflowState(self.student).setState('school fee paid')
+        delta = timedelta(days=180)
+        self.student.personal_updated = datetime.utcnow() - delta
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.student_path)
+        self.assertTrue(
+            'You logged in' in self.browser.contents)
+        # Students don't see personal_updated field in edit form
+        self.browser.open(self.edit_personal_path)
+        self.assertFalse('Updated' in self.browser.contents)
+        self.browser.open(self.personal_path)
+        self.assertTrue('Updated' in self.browser.contents)
+        self.browser.getLink("Logout").click()
+        delta = timedelta(days=181)
+        self.student.personal_updated = datetime.utcnow() - delta
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.edit_personal_path)
+        self.assertTrue(
+            'Your personal data record is outdated.' in self.browser.contents)
+
+    def test_request_transcript(self):
+        IWorkflowState(self.student).setState('graduated')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertMatches(
+            '...You logged in...', self.browser.contents)
+        # Create payment ticket
+        self.browser.open(self.payments_path)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['transcript']
+        self.browser.getControl("Create ticket").click()
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 4567.0)
+        # Student is the payer of the payment ticket.
+        payer = IPayer(self.student['payments'][value])
+        self.assertEqual(payer.display_fullname, 'Anna Tester')
+        self.assertEqual(payer.id, self.student_id)
+        self.assertEqual(payer.faculty, 'fac1')
+        self.assertEqual(payer.department, 'dep1')
+        # We simulate the approval and fetch the pin
+        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
+        self.browser.open(self.browser.url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+        pin = self.app['accesscodes']['TSC-0'].keys()[0]
+        parts = pin.split('-')[1:]
+        tscseries, tscnumber = parts
+        # Student can use the pin to send the transcript request
+        self.browser.open(self.student_path)
+        self.browser.getLink("Request transcript").click()
+        self.browser.getControl(name="ac_series").value = tscseries
+        self.browser.getControl(name="ac_number").value = tscnumber
+        self.browser.getControl(name="comment").value = 'Comment line 1 \nComment line2'
+        self.browser.getControl(name="address").value = 'Address line 1 \nAddress line2'
+        self.browser.getControl("Submit").click()
+        self.assertMatches('...Transcript processing has been started...',
+                          self.browser.contents)
+        self.assertEqual(self.student.state, 'transcript requested')
+        self.assertMatches(
+            '... UTC K1000000 wrote:\n\nComment line 1 \n'
+            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
+            'Address line2\n\n', self.student['studycourse'].transcript_comment)
+        # The comment has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'K1000000 - students.browser.StudentTranscriptRequestPage - '
+            'K1000000 - comment: Comment line 1 <br>Comment line2\n'
+            in logcontent)
+
+    def test_late_registration(self):
+        # Login
+        delta = timedelta(days=10)
+        self.app['configuration'][
+            '2004'].coursereg_deadline = datetime.now(pytz.utc) - delta
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.browser.open(self.payments_path)
+        self.browser.open(self.payments_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['late_registration']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        self.browser.open(self.payments_path)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        self.browser.getLink("100").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getControl("Register course list").click()
+        self.assertTrue('Course registration has ended. Please pay' in self.browser.contents)
+        self.student['payments'][value].approve()
+        self.browser.getControl("Register course list").click()
+        self.assertTrue('Course list has been registered' in self.browser.contents)
+        self.assertEqual(self.student.state, 'courses registered')
+
+    def test_former_course(self):
+        IWorkflowState(self.student).setState('school fee paid')
+        self.student['studycourse'].current_level = 100
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        # Now students can add the current study level
+        self.browser.getLink("Study Course").click()
+        self.browser.getLink("Add course list").click()
+        self.assertMatches('...Add current level 100 (Year 1)...',
+                           self.browser.contents)
+        self.browser.getControl("Create course list now").click()
+        # A level with one course ticket was created
+        self.assertEqual(
+            self.student['studycourse']['100'].number_of_tickets, 1)
+        self.browser.getLink("100").click()
+        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
+        self.browser.getLink("Edit course list").click()
+        del self.student['studycourse']['100']['COURSE1']
+        # Course can be added again via CourseTicketAddFormPage2
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Successfully added COURSE1' in self.browser.contents)
+        self.assertEqual(len(self.student['studycourse']['100'].keys()),1)
+        del self.student['studycourse']['100']['COURSE1']
+        self.course.former_course = True
+        self.browser.getLink("here").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Former courses can\'t be added.' in self.browser.contents)
+        self.assertEqual(len(self.student['studycourse']['100'].keys()),0)
+        # Course can be added again via StudyLevelEditFormPage
+        self.browser.getLink("100").click()
+        self.browser.getLink("Edit course list").click()
+        self.browser.getControl(name="course").value = 'COURSE1'
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Former courses can\'t be added.' in
+            self.browser.contents)
+        self.assertEqual(len(self.student['studycourse']['100'].keys()),0)
+        # but added if current course
+        self.course.former_course = False
+        self.browser.getControl(name="course").value = 'COURSE1'
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Successfully added COURSE1' in self.browser.contents)
+        self.assertEqual(len(self.student['studycourse']['100'].keys()),1)
+        return
+
+class StudentRequestPWTests(StudentsFullSetup):
+    # Tests for student registration
+
+    layer = FunctionalLayer
+
+    def test_request_pw(self):
+        # Student with wrong number can't be found.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.number").value = 'anynumber'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No student record found.'
+            in self.browser.contents)
+        # Anonymous is not informed that lastname verification failed.
+        # It seems that the record doesn't exist.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.lastname").value = 'Johnny'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('No student record found.'
+            in self.browser.contents)
+        # Even with the correct lastname we can't register if a
+        # password has been set and used.
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your password has already been set and used.'
+            in self.browser.contents)
+        self.browser.open('http://localhost/app/requestpw')
+        self.app['students'][self.student_id].password = None
+        # The lastname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.lastname").value = 'tESTer'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        # Yeah, we succeded ...
+        self.assertTrue('Your password request was successful.'
+            in self.browser.contents)
+        # We can also use the matric_number instead.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.lastname").value = 'tESTer'
+        self.browser.getControl(name="form.number").value = '234'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
+        self.assertTrue('Your password request was successful.'
+            in self.browser.contents)
+        # ... and  student can be found in the catalog via the email address
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(
+            email=('new@yy.zz', 'new@yy.zz')))
+        self.assertEqual(self.student,results[0])
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - students.browser.StudentRequestPasswordPage - '
+                        '234 (K1000000) - new@yy.zz' in logcontent)
+        return
+
+    def test_student_locked_level_forms(self):
+
+        # Add two study levels, one current and one previous
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        IWorkflowState(self.student).setState('school fee paid')
+        self.student['studycourse'].current_level = 200
+
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse/100/edit')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/ctadd')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse/100/ctadd')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        IWorkflowState(self.student).setState('courses registered')
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse/200/ctadd')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+
+class PublicPagesTests(StudentsFullSetup):
+    # Tests for simple webservices
+
+    layer = FunctionalLayer
+
+    def test_paymentrequest(self):
+        payment = createObject('waeup.StudentOnlinePayment')
+        payment.p_category = u'schoolfee'
+        payment.p_session = self.student.current_session
+        payment.p_item = u'My Certificate'
+        payment.p_id = u'anyid'
+        self.student['payments']['anykey'] = payment
+        # Request information about unpaid payment ticket
+        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
+        self.assertEqual(self.browser.contents, '-1')
+        # Request information about paid payment ticket
+        payment.p_state = u'paid'
+        notify(grok.ObjectModifiedEvent(payment))
+        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
+        self.assertEqual(self.browser.contents,
+            'FULL_NAME=Anna Tester&FACULTY=fac1&DEPARTMENT=dep1'
+            '&PAYMENT_ITEM=My Certificate&PAYMENT_CATEGORY=School Fee'
+            '&ACADEMIC_SESSION=2004/2005&MATRIC_NUMBER=234&REG_NUMBER=123'
+            '&FEE_AMOUNT=0.0')
+        self.browser.open('http://localhost/app/paymentrequest?NONSENSE=nonsense')
+        self.assertEqual(self.browser.contents, '-1')
+        self.browser.open('http://localhost/app/paymentrequest?P_ID=nonsense')
+        self.assertEqual(self.browser.contents, '-1')
+
+class StudentDataExportTests(StudentsFullSetup, FunctionalAsyncTestCase):
+    # Tests for StudentsContainer class views and pages
+
+    layer = FunctionalLayer
+
+    def wait_for_export_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def add_payment(self, student):
+        # get a payment with all fields set
+        payment = StudentOnlinePayment()
+        payment.creation_date = datetime(2012, 12, 13)
+        payment.p_id = 'my-id'
+        payment.p_category = u'schoolfee'
+        payment.p_state = 'paid'
+        payment.ac = u'666'
+        payment.p_item = u'p-item'
+        payment.p_level = 100
+        payment.p_session = curr_year - 6
+        payment.payment_date = datetime(2012, 12, 13)
+        payment.amount_auth = 12.12
+        payment.r_amount_approved = 12.12
+        payment.r_code = u'r-code'
+        # XXX: there is no addPayment method to give predictable names
+        self.payment = student['payments']['my-payment'] = payment
+        return payment
+
+    def test_datacenter_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
+            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
+            '13/12/2012, 14/12/2012, all, all, all, all, all), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVView '
+            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def test_datacenter_export_selected(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@exportselected')
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="students").value = 'K1000000'
+        self.browser.getControl("Create CSV file").click()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            'adm_code,clr_code,date_of_birth,email,employer,'
+            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
+            'officer_comment,parents_email,perm_address,'
+            'personal_updated,phone,reg_number,'
+            'sex,student_id,suspended,suspended_comment,'
+            'password,state,history,certcode,is_postgrad,'
+            'current_level,current_session\r\n'
+            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,,'
+            '1234#,123,m,K1000000,0,,{SSHA}' in self.browser.contents)
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getControl("Discard").click()
+
+    def test_payment_dates(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        # If one payment date is missing, an error message appears
+        self.browser.getControl(name="payments_end").value = ''
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Payment dates do not match format d/m/Y'
+            in self.browser.contents)
+
+    def test_faculties_export(self):
+        self.add_payment(self.student)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        facs_path = 'http://localhost/app/faculties'
+        self.browser.open(facs_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
+        self.browser.getControl(name="paycat").value = ['schoolfee']
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(facs_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            '666,12.12,2012-12-13 00:00:00#,schoolfee,[],1,my-id,p-item,100,%s,'
+            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
+            'Tester,created,2004,2004,,fac1,dep1,CERT1'
+            %(curr_year-6) in self.browser.contents)
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open(facs_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.FacultiesExportJobContainerJobConfig '
+            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
+            '13/12/2012, 14/12/2012, all, all, schoolfee, all, all), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+        # Officer can also enter student id and gets the same export file
+        self.browser.open(facs_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Enter student ids or matric numbers").click()
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="students").value = 'K1000000'
+        self.browser.getControl("Create CSV file").click()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/faculties/exports')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            '666,12.12,2012-12-13 00:00:00#,schoolfee,[],1,my-id,p-item,100,%s,'
+            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
+            'Tester,created,2004,2004,,fac1,dep1,CERT1'
+            %(curr_year-6) in self.browser.contents)
+
+    def test_faculty_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        fac1_path = 'http://localhost/app/faculties/fac1'
+        self.browser.open(fac1_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        # The testbrowser does not hide the payment period fields, but
+        # values are ignored when using the students exporter.
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
+        self.browser.getControl("Create CSV file").click()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(fac1_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            'adm_code,clr_code,date_of_birth,email,employer,'
+            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
+            'officer_comment,parents_email,perm_address,'
+            'personal_updated,phone,reg_number,'
+            'sex,student_id,suspended,suspended_comment,'
+            'password,state,history,certcode,is_postgrad,'
+            'current_level,current_session\r\n'
+            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,,'
+            '1234#,123,m,K1000000,0,,{SSHA}' in self.browser.contents)
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open(fac1_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.FacultyExportJobContainerJobConfig '
+            '- exported: students (2004, 100, ug_ft, fac1, None, None, '
+            '13/12/2012, 14/12/2012, all, all, all, all, all), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+        # Officer can set export parameters but cannot enter student id
+        # at faculty level
+        self.browser.open(fac1_path + '/exports')
+        self.assertTrue("Set export parameters"
+                         in self.browser.contents)
+        self.assertFalse("Enter student ids or matric numbers"
+                         in self.browser.contents)
+
+    def test_department_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        dep1_path = 'http://localhost/app/faculties/fac1/dep1'
+        self.browser.open(dep1_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        # The testbrowser does not hide the payment period fields, but
+        # values are ignored when using the students exporter.
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(dep1_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open(dep1_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.DepartmentExportJobContainerJobConfig '
+            '- exported: students (2004, 100, ug_ft, None, dep1, None, '
+            '13/12/2012, 14/12/2012, all, all, all, all, all), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def test_certificate_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        cert1_path = 'http://localhost/app/faculties/fac1/dep1/certificates/CERT1'
+        self.browser.open(cert1_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(cert1_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open(cert1_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.CertificateExportJobContainerJobConfig '
+            '- exported: students '
+            '(2004, 100, None, None, None, CERT1, , , None, None, '
+            'None, None, None), '
+            'job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def deprecated_test_course_export_students(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
+        self.browser.open(course1_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['students']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(course1_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open(course1_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
+            '- exported: students (2004, 100, COURSE1), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def test_course_export_lecturer(self):
+        # We add study level 100 to the student's studycourse
+        studylevel = StudentStudyLevel()
+        studylevel.level = 100
+        studylevel.level_session = 2004
+        IWorkflowState(self.student).setState('courses validated')
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate,studylevel)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
+        self.browser.open(course1_path)
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.assertTrue(
+            'Academic session not set. Please contact the administrator.'
+            in self.browser.contents)
+        self.app['configuration'].current_academic_session = 2004
+        self.browser.getControl("Set export parameters").click()
+        self.browser.getControl(name="exporter").value = ['lecturer']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(course1_path + '/exports')
+        # ... the csv file can be downloaded ...
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_lecturer_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        # ... and contains the course ticket COURSE1
+        self.assertEqual(self.browser.contents,
+            'matric_number,student_id,display_fullname,level,code,'
+            'level_session,score\r\n234,K1000000,Anna Tester,'
+            '100,COURSE1,2004,\r\n')
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # Thew job can be discarded
+        self.browser.open(course1_path + '/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
+            '- exported: lecturer (2004, 100, COURSE1), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_lecturer_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def test_export_departmet_officers(self):
+        # Create department officer
+        self.app['users'].addUser('mrdepartment', SECRET)
+        self.app['users']['mrdepartment'].email = 'mrdepartment@foo.ng'
+        self.app['users']['mrdepartment'].title = 'Carlo Pitter'
+        # Assign local role
+        department = self.app['faculties']['fac1']['dep1']
+        prmlocal = IPrincipalRoleManager(department)
+        prmlocal.assignRoleToPrincipal('waeup.local.DepartmentOfficer', 'mrdepartment')
+        # Login as department officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrdepartment'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.browser.open("http://localhost/app/faculties/fac1/dep1")
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        # Only the sfpaymentsoverview exporter is available for department officers
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue(
+            '<option value="sfpaymentsoverview">' in self.browser.contents)
+        self.browser.getControl(name="exporter").value = ['sfpaymentsoverview']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Export started' in self.browser.contents)
+        # Thew job can be discarded
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        self.wait_for_export_job_completed()
+        self.browser.open("http://localhost/app/faculties/fac1/dep1/exports")
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+
+    def test_export_bursary_officers(self):
+        self.add_payment(self.student)
+        # Create bursary officer
+        self.app['users'].addUser('mrbursary', SECRET)
+        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
+        self.app['users']['mrbursary'].title = 'Carlo Pitter'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
+        # Login as bursary officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrbursary'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.browser.getLink("Academics").click()
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        # Only the bursary exporter is available for bursary officers
+        # not only at facultiescontainer level ...
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue('<option value="bursary">' in self.browser.contents)
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Export started' in self.browser.contents)
+        # ... but also at other levels
+        self.browser.open('http://localhost/app/faculties/fac1/dep1')
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue('<option value="bursary">' in self.browser.contents)
+        # Thew job can be downloaded
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open('http://localhost/app/faculties/exports')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            '666,12.12,2012-12-13 00:00:00#,schoolfee,[],1,my-id,p-item,100,%s,'
+            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
+            'Tester,created,2004,2004,,fac1,dep1,CERT1'
+            %(curr_year-6) in self.browser.contents)
+        # ... and discarded
+        self.browser.open('http://localhost/app/faculties/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # At Academics level bursary officers can also enter student ids
+        self.browser.getLink("Academics").click()
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Enter student ids or matric numbers").click()
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="students").value = 'K1000000'
+        self.browser.getControl("Create CSV file").click()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/faculties/exports')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertTrue(
+            '666,12.12,2012-12-13 00:00:00#,schoolfee,[],1,my-id,p-item,100,%s,'
+            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
+            'Tester,created,2004,2004,,fac1,dep1,CERT1'
+            %(curr_year-6) in self.browser.contents)
+
+    def test_export_accommodation_officers(self):
+        # Create bursary officer
+        self.app['users'].addUser('mracco', SECRET)
+        self.app['users']['mracco'].email = 'mracco@foo.ng'
+        self.app['users']['mracco'].title = 'Carlo Pitter'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.AccommodationOfficer', 'mracco')
+        # Login as bursary officer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mracco'
+        self.browser.getControl(name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.browser.getLink("Academics").click()
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        # accommodationpayments and beds exporters are available
+        # not only at facultiescontainer level ...
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue('<option value="accommodationpayments">'
+            in self.browser.contents)
+        self.assertTrue('<option value="bedtickets">' in self.browser.contents)
+        self.browser.getControl(
+            name="exporter").value = ['accommodationpayments']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Export started' in self.browser.contents)
+        # ... but also at other levels
+        self.browser.open('http://localhost/app/faculties/fac1/dep1')
+        self.browser.getLink("Export student data").click()
+        self.browser.getControl("Set export parameters").click()
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue('<option value="accommodationpayments">'
+            in self.browser.contents)
+        self.assertTrue('<option value="bedtickets">' in self.browser.contents)
+        # Thew job can be discarded
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        self.wait_for_export_job_completed()
+        self.browser.open('http://localhost/app/faculties/exports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+
+
+UPLOAD_CSV_TEMPLATE = (
+    'matric_number,student_id,display_fullname,level,code,level_session,'
+    'score\r\n'
+    '234,K1000000,Anna Tester,100,COURSE1,2004,%s\r\n')
+
+class LecturerUITests(StudentsFullSetup):
+    # Tests for UI actions when acting as lecturer.
+
+    def login_as_lecturer(self):
+        self.app['users'].addUser('mrslecturer', SECRET)
+        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
+        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
+        # Add course ticket
+        self.studylevel = createObject(u'waeup.StudentStudyLevel')
+        self.studylevel.level = 100
+        self.studylevel.level_session = 2004
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, self.studylevel)
+        # Assign local Lecturer role for a course.
+        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        prmlocal = IPrincipalRoleManager(course)
+        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
+        notify(LocalRoleSetEvent(
+            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
+        # Login as lecturer.
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrslecturer'
+        self.browser.getControl(
+            name="form.password").value = SECRET
+        self.browser.getControl("Login").click()
+        # Store reused urls/paths
+        self.course_url = (
+            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1')
+        self.edit_scores_url = '%s/edit_scores' % self.course_url
+        # Set standard parameters
+        self.app['configuration'].current_academic_session = 2004
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        IWorkflowState(self.student).setState(VALIDATED)
+
+    @property
+    def stud_log_path(self):
+        return os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+
+    def test_lecturer_lands_on_landing_page(self):
+        # lecturers can login and will be led to landing page.
+        self.login_as_lecturer()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
+        self.assertTrue(
+            "<span>Unnamed Course (COURSE1)</span>"
+            in self.browser.contents)
+
+    def test_lecturer_department_role(self):
+        # lecturers can login and will be led to landing page also if
+        # role is assigned at department level.
+        self.login_as_lecturer()
+        # we remove the role granted above
+        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        prmlocal = IPrincipalRoleManager(course)
+        prmlocal.removeRoleFromPrincipal('waeup.local.Lecturer', 'mrslecturer')
+        notify(LocalRoleSetEvent(
+            course, 'waeup.local.Lecturer', 'mrslecturer', granted=False))
+        self.browser.open(URL_LECTURER_LANDING)
+        # no course appears
+        self.assertFalse(
+            "<span>Unnamed Course (COURSE1)</span>"
+            in self.browser.contents)
+        # we assign lecturer at department level
+        dep = self.app['faculties']['fac1']['dep1']
+        prmlocal = IPrincipalRoleManager(dep)
+        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
+        notify(LocalRoleSetEvent(
+            dep, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
+        self.browser.open(URL_LECTURER_LANDING)
+        # course appears again
+        self.assertTrue(
+            "<span>Unnamed Course (COURSE1)</span>"
+            in self.browser.contents)
+
+    def test_my_roles_link_works(self):
+        # lecturers can see their roles
+        self.login_as_lecturer()
+        self.browser.getLink("My Roles").click()
+        self.assertTrue(
+            "<div>Academics Officer (view only)</div>"
+            in self.browser.contents)
+        self.assertTrue(
+            '<a href="%s">' % self.course_url in self.browser.contents)
+
+    def test_my_roles_page_contains_backlink(self):
+        # we can get back from 'My Roles' view to landing page
+        self.login_as_lecturer()
+        self.browser.getLink("My Roles").click()
+        self.browser.getLink("My Courses").click()
+        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
+
+    def test_lecturers_can_reach_their_courses(self):
+        # lecturers get links to their courses on the landing page
+        self.login_as_lecturer()
+        self.browser.getLink("COURSE1").click()
+        self.assertEqual(self.browser.url, self.course_url)
+
+    def test_lecturers_student_access_is_restricted(self):
+        # lecturers are not able to change other student data
+        self.login_as_lecturer()
+        # Lecturers can neither filter students ...
+        self.assertRaises(
+            Unauthorized, self.browser.open, '%s/students' % self.course_url)
+        # ... nor access the student ...
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.student_path)
+        # ... nor the respective course ticket since editing course
+        # tickets by lecturers is not feasible.
+        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
+        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
+        self.assertRaises(
+            Unauthorized, self.browser.open, course_ticket_path)
+
+    def test_score_editing_requires_department_permit(self):
+        # we get a warning if we try to update score while we are not allowed
+        self.login_as_lecturer()
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
+        self.browser.open(self.course_url)
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertTrue('Score editing disabled' in self.browser.contents)
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        self.browser.open(self.course_url)
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertFalse('Score editing disabled' in self.browser.contents)
+
+    def test_score_editing_requires_validated_students(self):
+        # we can edit only scores of students whose courses have been
+        # validated.
+        self.login_as_lecturer()
+        # set invalid student state
+        IWorkflowState(self.student).setState(CREATED)
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # set valid student state
+        IWorkflowState(self.student).setState(VALIDATED)
+        self.browser.open(self.edit_scores_url)
+        self.assertTrue(
+            self.browser.getControl(name="scores:list") is not None)
+
+    def test_score_editing_offers_only_current_scores(self):
+        # only scores from current academic session can be edited
+        self.login_as_lecturer()
+        IWorkflowState(self.student).setState('courses validated')
+        # with no academic session set
+        self.app['configuration'].current_academic_session = None
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # with wrong academic session set
+        self.app['configuration'].current_academic_session = 1999
+        self.browser.open(self.edit_scores_url)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+        # with right academic session set
+        self.app['configuration'].current_academic_session = 2004
+        self.browser.reload()
+        self.assertTrue(
+            self.browser.getControl(name="scores:list") is not None)
+        # if level_session of studycourse changes, catalog has been updated
+        # and student disappears
+        self.studylevel.level_session = 2005
+        self.browser.reload()
+        self.assertRaises(
+            LookupError, self.browser.getControl, name="scores")
+
+    def test_score_editing_can_change_scores(self):
+        # we can really change scores via edit_scores view
+        self.login_as_lecturer()
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, None)
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = '55'
+        self.browser.getControl("Update scores").click()
+        # the new value is stored in data
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 55)
+        # the new value is displayed on page/prefilled in form
+        self.assertEqual(
+            self.browser.getControl(name="scores:list", index=0).value, '55')
+        # The change has been logged
+        with open(self.stud_log_path, 'r') as fd:
+            self.assertTrue(
+                'mrslecturer - students.browser.EditScoresPage - '
+                'K1000000 100/COURSE1 score updated (55)' in fd.read())
+
+    def test_scores_editing_scores_must_be_integers(self):
+        # Non-integer scores won't be accepted.
+        self.login_as_lecturer()
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = 'abc'
+        self.browser.getControl("Update scores").click()
+        self.assertTrue(
+            'Error: Score(s) of following students have not been updated '
+            '(only integers are allowed): Anna Tester.'
+            in self.browser.contents)
+
+    def test_scores_editing_allows_score_removal(self):
+        # we can remove scores, once they were set
+        self.login_as_lecturer()
+        # without a prior value, we cannot remove
+        self.student['studycourse']['100']['COURSE1'].score = None
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        logcontent = open(self.stud_log_path, 'r').read()
+        self.assertFalse('COURSE1 score updated (None)' in logcontent)
+        # now retry with some value set
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.getControl(name="scores:list", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        logcontent = open(self.stud_log_path, 'r').read()
+        self.assertTrue('COURSE1 score updated (None)' in logcontent)
+
+    def test_lecturer_can_validate_courses(self):
+        # the form is locked after validation
+        self.login_as_lecturer()
+        self.student['studycourse']['100']['COURSE1'].score = None
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        self.browser.getControl("Validate").click()
+        self.assertTrue(
+            'No score has been entered.' in self.browser.contents)
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl(name="scores:list", index=0).value = '66'
+        self.browser.getControl("Update scores").click()
+        self.browser.getControl("Validate").click()
+        self.assertTrue(
+            'You successfully validated the course results'
+            in self.browser.contents)
+        self.assertEqual(self.course.results_validation_session, 2004)
+        self.assertEqual(self.course.results_validated_by, 'Mercedes Benz')
+        self.assertEqual(self.browser.url, self.course_url)
+        # Lecturer can't open edit_scores again
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertEqual(self.browser.url, self.course_url)
+        self.assertTrue(
+            'Course results have already been validated'
+            ' and can no longer be changed.'
+            in self.browser.contents)
+        # Also DownloadScoresView is blocked
+        self.browser.open(self.browser.url + '/download_scores')
+        self.assertEqual(self.browser.url, self.course_url)
+        self.assertTrue(
+            'Course results have already been validated'
+            ' and can no longer be changed.'
+            in self.browser.contents)
+        # Students Manager can open page ...
+        prmlocal = IPrincipalRoleManager(self.course)
+        prmlocal.assignRoleToPrincipal(
+            'waeup.local.LocalStudentsManager', 'mrslecturer')
+        self.browser.getLink("Update session 2004/2005 scores").click()
+        self.assertEqual(self.browser.url, self.edit_scores_url)
+        self.browser.getLink("Download csv file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        # ... but can't validate courses a second time
+        self.browser.open(self.edit_scores_url)
+        self.browser.getControl("Validate").click()
+        self.assertTrue(
+            'Course results have already been validated.'
+            in self.browser.contents)
+
+    def test_lecturers_can_download_course_tickets(self):
+        # A course ticket slip can be downloaded
+        self.course.title = (u'Lorem ipsum     dolor sit amet, consectetur     adipisici, '
+                             u'sed         eiusmod tempor    incidunt ut  labore et dolore')
+        self.login_as_lecturer()
+        pdf_url = '%s/coursetickets.pdf' % self.course_url
+        self.browser.open(pdf_url)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'coursetickets.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF coursetickets.pdf written to %s" % path
+
+    def test_lecturers_can_download_attendance_sheet(self):
+        # A course ticket slip can be downloaded
+        self.course.title = (u'Lorem ipsum     dolor sit amet, consectetur     adipisici, '
+                             u'sed         eiusmod tempor    incidunt ut  labore et dolore')
+        self.student.firstname = u'Emmanuella Woyengifigha Mercy Onosemudiana'
+        self.student.lastname = u'OYAKEMIEGBEGHA'
+        self.student.matric_number = u'hdk7gd62i872z27zt27ge'
+        self.login_as_lecturer()
+        pdf_url = '%s/attendance.pdf' % self.course_url
+        self.browser.open(pdf_url)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(
+            self.browser.headers['Content-Type'], 'application/pdf')
+        path = os.path.join(samples_dir(), 'attendance.pdf')
+        open(path, 'wb').write(self.browser.contents)
+        print "Sample PDF attendance.pdf written to %s" % path
+
+
+    def test_lecturers_can_download_scores_as_csv(self):
+        # Lecturers can download course scores as CSV.
+        self.login_as_lecturer()
+        self.browser.open(self.edit_scores_url)
+        self.browser.getLink("Download csv file").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'text/csv; charset=UTF-8')
+        self.assertEqual(self.browser.contents, 'matric_number,student_id,'
+            'display_fullname,level,code,level_session,score\r\n234,'
+            'K1000000,Anna Tester,100,COURSE1,2004,\r\n')
+
+    def test_scores_csv_upload_available(self):
+        # lecturers can upload a CSV file to set values.
+        self.login_as_lecturer()
+        # set value to change from
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        upload_ctrl = self.browser.getControl(name='uploadfile:file')
+        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % '65')
+        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
+        self.browser.getControl("Update editable scores from").click()
+        # value changed
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 65)
+
+    def test_scores_csv_upload_ignored(self):
+        # for many type of file contents we simply ignore uploaded data
+        self.login_as_lecturer()
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        for content, mimetype, name in (
+                # empty file
+                ('', 'text/foo', 'my.foo'),
+                # plain ASCII text, w/o comma
+                ('abcdef' * 200, 'text/plain', 'my.txt'),
+                # plain UTF-8 text, with umlauts
+                ('umlauts: äöü', 'text/plain', 'my.txt'),
+                # csv file with only a header row
+                ('student_id,score', 'text/csv', 'my.csv'),
+                # csv with student_id column missing
+                ('foo,score\r\nbar,66\r\n', 'text/csv', 'my.csv'),
+                # csv with score column missing
+                ('student_id,foo\r\nK1000000,bar\r\n', 'text/csv', 'my.csv'),
+                # csv with non number as score value
+                (UPLOAD_CSV_TEMPLATE % 'not-a-number', 'text/csv', 'my.csv'),
+                ):
+            upload_ctrl = self.browser.getControl(name='uploadfile:file')
+            upload_ctrl.add_file(StringIO(content), mimetype, name)
+            self.browser.getControl("Update scores").click()
+            self.assertEqual(
+                self.student['studycourse']['100']['COURSE1'].score, 55)
+            self.assertFalse(
+                'Uploaded file contains illegal data' in self.browser.contents)
+
+    def test_scores_csv_upload_warn_illegal_chars(self):
+        # for some types of files we issue a warning if upload data
+        # contains illegal chars (and ignore the data)
+        self.login_as_lecturer()
+        self.student['studycourse']['100']['COURSE1'].score = 55
+        self.browser.open(self.edit_scores_url)
+        for content, mimetype, name in (
+                # plain ASCII text, commas, control chars
+                ('abv,qwe\n\r\r\t\b\n' * 20, 'text/plain', 'my.txt'),
+                # image data (like a JPEG image)
+                (open(SAMPLE_IMAGE, 'rb').read(), 'image/jpg', 'my.jpg'),
+                ):
+            upload_ctrl = self.browser.getControl(name='uploadfile:file')
+            upload_ctrl.add_file(StringIO(content), mimetype, name)
+            self.browser.getControl("Update editable scores").click()
+            self.assertEqual(
+                self.student['studycourse']['100']['COURSE1'].score, 55)
+            self.assertTrue(
+                'Uploaded file contains illegal data' in self.browser.contents)
+
+class ParentsUITests(StudentsFullSetup):
+    # Tests for UI actions when acting as parents.
+
+    def test_request_ppw(self):
+        self.app['students'][self.student_id].parents_email = 'par@yy.zz'
+        self.browser.open('http://localhost/app/requestppw')
+        self.browser.getControl(name="form.lastname").value = 'tESTer'
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl(name="form.email").value = 'par@yy.zz'
+        self.browser.getControl("Send temporary login credentials").click()
+        self.assertTrue('Your password request was successful.'
+            in self.browser.contents)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - students.browser.RequestParentsPasswordPage - '
+                        '123 (K1000000) - par@yy.zz' in logcontent)
+        return
+
+    def test_login_as_parents(self):
+        # Student login still works after all the changes made
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue('You logged in' in self.browser.contents)
+        self.browser.open(self.edit_personal_path)
+        self.browser.getLink("Logout").click()
+        self.assertTrue('You have been logged out' in self.browser.contents)
+        # We set parents password
+        self.app['students'][self.student_id].setParentsPassword('ppwd')
+        self.browser.open(self.login_path)
+        # Student can't login with original password
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.assertEqual(self.browser.url, self.login_path)
+        self.assertTrue('Your account has been temporarily deactivated '
+            'because your parents have logged in.' in self.browser.contents)
+        # Parents can login with their password
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'ppwd'
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
+        self.assertTrue(
+            '<a href="http://localhost/app/students/K1000000">Base Data</a>'
+            in self.browser.contents)
+        # They do not see all links ...
+        self.assertFalse(
+            '<a href="http://localhost/app/students/K1000000/history">History</a>'
+            in self.browser.contents)
+        # ... and can't change anything
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.edit_personal_path)
+        # If the password has expired, parents are logged out and the
+        # student can login again with the original password
+        delta = timedelta(minutes=11)
+        self.app['students'][self.student_id].parents_password[
+            'timestamp'] = datetime.utcnow() - delta
+        self.app['students'][self.student_id]._p_changed = True
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.student_path)
+        # Parents login is written to log file
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'K1000000 - browser.pages.LoginPage - K1000000 - Parents logged in'
+            in logcontent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_catalog.py	(revision 16045)
@@ -0,0 +1,286 @@
+## $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
+##
+import grok
+import shutil
+import tempfile
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, createObject
+from zope.component.hooks import setSite
+from waeup.kofa.app import University
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.students.student import Student
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+
+class CatalogTestSetup(FunctionalTestCase):
+    # A setup for testing catalog related stuff.
+    #
+    # sets up a site with some student already created.
+    layer = FunctionalLayer
+
+    def create_cert(self, facname, deptname, certname):
+        # helper: create faculty, dept, and cert
+        self.app['faculties'][facname] = Faculty(code=facname)
+        self.app['faculties'][facname][deptname] = Department(code=deptname)
+        cert = createObject('waeup.Certificate')
+        cert.start_level = 100
+        cert.end_level = 500
+        cert.code = certname
+        self.app['faculties'][facname][deptname].certificates.addCertificate(
+            cert)
+        return cert
+
+    def setUp(self):
+        super(CatalogTestSetup, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+        self.certificate = self.create_cert(u'fac1', u'dep1', u'CERT1')
+        self.certificate.study_mode = u'ug_ft'
+
+        # Create student with subobjects
+        student = Student()
+        student.firstname = u'Bob'
+        student.lastname = u'Tester'
+        student.matric_number = u'1234'
+        self.app['students'].addStudent(student)
+        self.student_id = student.student_id
+        self.student = self.app['students'][self.student_id]
+        self.student['studycourse'].certificate = self.certificate
+        self.student['studycourse'].current_session = 2010
+        self.student['studycourse'].current_level = 100
+        # Update the students_catalog
+        notify(grok.ObjectModifiedEvent(self.student))
+        studylevel = createObject('waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2010
+        self.student['studycourse']['100'] = studylevel
+        ticket = createObject('waeup.CourseTicket')
+        ticket.code = 'Course1'
+        ticket.credits = 30
+        ticket.score = 70
+        self.student['studycourse']['100']['Course1'] = ticket
+        payment = createObject(u'waeup.StudentOnlinePayment')
+        payment.p_id = 'p1234567890'
+        payment.p_item = u'any item'
+        payment.p_session = 2010
+        payment.p_category = 'schoolfee'
+        payment.amount_auth = 12345.678
+        payment.p_state = 'paid'
+        self.student['payments'][payment.p_id] = payment
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(CatalogTestSetup, self).tearDown()
+        return
+
+class StudentCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an students catalog if we wish
+        cat = queryUtility(ICatalog, name='students_catalog')
+        assert cat is not None
+
+    def test_search_by_id(self):
+        # We can find a certain student id
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(student_id=(self.student_id,
+                                                self.student_id))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+    def test_search_by_name(self):
+        # We can find a certain name
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(fullname='Bob Tester')
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+    def test_search_by_department(self):
+        # We can find a student studying in a certain department
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(depcode=('dep1','dep1'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+    def test_search_by_faculty(self):
+        # We can find a student studying in a certain faculty
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(faccode=('fac1','fac1'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+    def test_search_by_session(self):
+        # We can find a student in a certain session
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(current_session=(2010,2010))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+    def test_search_by_level(self):
+        # We can find a student in a certain level
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(current_level=(100, 100))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+        return
+
+    def test_search_by_mode(self):
+        # We can find a student in a certain mode
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(current_mode=('ug_ft', 'ug_ft'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+        # Attention: A change of the certificate's study mode
+        # is not reflected by the students catalog. The students_catalog
+        # must be reindexed manually.
+        self.certificate.study_mode = u'ug_pt'
+        results = cat.searchResults(current_mode=('ug_ft', 'ug_ft'))
+        assert len(results) == 1
+        results = cat.searchResults(current_mode=('pg_ft', 'pg_ft'))
+        assert len(results) == 0
+        return
+
+
+class CourseTicketCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get an students catalog if we wish
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        assert cat is not None
+
+    def test_search_by_code(self):
+        # We can find a certain course ticket by its code
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        results = cat.searchResults(code=('Course1', 'Course1'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'studycourse']['100']['Course1']
+
+    def test_search_by_level(self):
+        # We can find a certain course ticket by the level
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        results = cat.searchResults(level=(100, 100))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'studycourse']['100']['Course1']
+
+    def test_search_by_session(self):
+        # We can find a certain course ticket by the level session
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        results = cat.searchResults(session=(2010, 2010))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'studycourse']['100']['Course1']
+
+class PaymentCatalogTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_get_catalog(self):
+        # We can get a students catalog if we wish
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        assert cat is not None
+
+    def test_search_by_session(self):
+        # We can find a payment ticket by the payment session
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_session=(2010, 2010))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
+
+    def test_search_by_category(self):
+        # We can find a payment ticket by the payment category
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_category=('schoolfee','schoolfee'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
+
+    def test_search_by_item(self):
+        # We can find a payment ticket by the payment item
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_item=('any item','any item'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
+
+    def test_search_by_state(self):
+        # We can find a payment ticket by the payment state
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_state=('paid','paid'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
+
+    def test_reindex(self):
+        # We can reindex any kind of catalog with the updateIndexes method.
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(p_state=('failed','failed'))
+        assert len(results) == 0
+        results = cat.searchResults(p_state=('paid','paid'))
+        assert len(results) == 1
+        results = [x for x in results] # Turn results generator into list
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
+        results[0].p_state = 'failed'
+        # Since we did not fire an ObjectModifiedEvent the catalog remained
+        # unchanged
+        results = cat.searchResults(p_state=('paid','paid'))
+        assert len(results) == 1
+        # Reindexing the catalog will lead to correct search results
+        #reindexPayments()
+        cat.clear() # The test works with and without clearing the catalog
+        cat.updateIndexes()
+        results = cat.searchResults(p_state=('paid','paid'))
+        assert len(results) == 0
+        results = cat.searchResults(p_state=('failed','failed'))
+        assert len(results) == 1
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id][
+            'payments']['p1234567890']
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_container.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_container.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_container.py	(revision 16045)
@@ -0,0 +1,103 @@
+## $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
+##
+"""
+Tests for students containers.
+"""
+import tempfile
+import shutil
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component.hooks import setSite, clearSite
+from waeup.kofa.app import University
+from waeup.kofa.university.department import Department
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer,
+    )
+from waeup.kofa.students.container import (
+    StudentsContainer, 
+    )
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger)
+
+class StudentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        remove_logger('waeup.kofa.app.students')
+        super(StudentsContainerTestCase, self).setUp()
+        # Setup a sample site for each test
+        # Prepopulate the ZODB...
+        app = University()
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+        return
+
+    def tearDown(self):
+        super(StudentsContainerTestCase, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_interfaces(self):
+        # Make sure the correct interfaces are implemented.
+        self.assertTrue(
+            verifyClass(
+                IStudentsContainer, StudentsContainer)
+            )
+        self.assertTrue(
+            verifyObject(
+                IStudentsContainer, StudentsContainer())
+            )
+        return
+
+    def test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = StudentsContainer()
+        # We cannot add arbitrary objects
+        department = Department()
+        self.assertRaises(
+            TypeError, container.addStudent, department)
+
+
+    def test_logger(self):
+        # We can get a logger from root
+        logger = self.app['students'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.kofa.app.students'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('students.log')
+        assert filename.startswith(self.dc_root)
+
+    def test_logger_multiple(self):
+        # Make sure the logger is still working after 2nd call
+        # First time we call it, it might be registered
+        logger = self.app['students'].logger
+        # At second call the already registered logger should be returned
+        logger = self.app['students'].logger
+        assert logger is not None
+        assert logger.name == 'waeup.kofa.app.students'
+        handlers = logger.handlers
+        assert len(handlers) == 1
+        filename = logger.handlers[0].baseFilename
+        assert filename.endswith('students.log')
+        assert filename.startswith(self.dc_root)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 16045)
@@ -0,0 +1,110 @@
+## $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
+##
+"""
+Tests for dynamic roles concerning students and related.
+"""
+from zope.interface import verify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.settings import Allow
+from zope.securitypolicy.tests.test_annotationprincipalrolemanager import (
+    Test as APRMTest, Manageable)
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.app import University
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.students import Student, StudentPrincipalRoleManager
+
+class StudentPrincipalRoleManagerTests(APRMTest):
+    # Make sure our PRM behaves like a regular one for usual cases.
+    # Usual cases are ones, that do not interact with external officers.
+    # See next test case for functional tests including officer perms.
+
+    def _make_roleManager(self, obj=None):
+        # Overriding this method of original testcase we make sure
+        # that really a Student PRM is tested.
+        if obj is None:
+            #obj = Manageable()
+            obj = Student()
+            obj['studycourse'] = Manageable()
+        return StudentPrincipalRoleManager(obj)
+
+class StudentPrincipalRoleManagerFunctionalTests(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentPrincipalRoleManagerFunctionalTests, self).setUp()
+        # assign permissions for a virtual officers
+        prm = IPrincipalRoleManager(self.app['faculties']['fac1']['dep1'])
+        prm.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'alice')
+        prm.assignRoleToPrincipal('waeup.local.PGClearanceOfficer', 'bob')
+        prm.assignRoleToPrincipal('waeup.local.UGClearanceOfficer', 'anne')
+        prm.assignRoleToPrincipal('waeup.local.LocalStudentsManager', 'benita')
+        prm.assignRoleToPrincipal('waeup.local.LocalWorkflowManager', 'benita')
+        return
+
+    def test_iface(self):
+        # make sure our StudentPRM really implements required ifaces.
+        obj = StudentPrincipalRoleManager(self.student)
+        verify.verifyClass(IPrincipalRoleManager,
+                           StudentPrincipalRoleManager)
+        verify.verifyObject(IPrincipalRoleManager, obj)
+        return
+
+    def test_get_as_adapter(self):
+        # we can get an StudentPRM for Students by adapter lookup
+        prm = IPrincipalRoleManager(self.student)
+        self.assertTrue(
+            isinstance(prm, StudentPrincipalRoleManager))
+        return
+
+    def test_no_officer_set(self):
+        # if the faculty/dept. of the connected cert has no local
+        # roles set, we won't get any additional roles for our
+        # student
+        prm = IPrincipalRoleManager(self.student)
+        result = prm.getRolesForPrincipal('claus')
+        self.assertEqual(result, [])
+        return
+
+    def test_valid_officer(self):
+        # for an officer that has clearance role on the connected dept
+        # we get the ClearanceOfficer role on our student
+        prm = IPrincipalRoleManager(self.student)
+        result = prm.getRolesForPrincipal('alice')
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        # Student is a UG student
+        self.assertFalse(self.student.is_postgrad)
+        result = prm.getRolesForPrincipal('bob')
+        self.assertEqual(result, [('waeup.StudentsOfficer', Allow)])
+        result = prm.getRolesForPrincipal('anne')
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        # Make student a PG student
+        self.certificate.study_mode = u'pg_ft'
+        self.assertTrue(self.student.is_postgrad)
+        result = prm.getRolesForPrincipal('bob')
+        # The dynamic roles changed
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        result = prm.getRolesForPrincipal('anne')
+        self.assertEqual(result, [('waeup.StudentsOfficer', Allow)])
+        # Multiple roles can be assigned
+        result = prm.getRolesForPrincipal('benita')
+        self.assertEqual(result, [
+            ('waeup.WorkflowManager', Allow),
+            ('waeup.StudentsManager', Allow)
+            ])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_export.py	(revision 16045)
@@ -0,0 +1,1649 @@
+## $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
+##
+
+import os
+import grok
+import datetime
+from cStringIO import StringIO
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.interfaces import (
+    ICSVExporter, IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.students.catalog import StudentsQuery
+from waeup.kofa.university.course import Course
+from waeup.kofa.university.certificate import CertificateCourse
+from waeup.kofa.students.export import (
+    StudentExporter, StudentStudyCourseExporter, StudentStudyLevelExporter,
+    CourseTicketExporter, StudentPaymentExporter, BedTicketExporter,
+    SchoolFeePaymentsOverviewExporter, StudyLevelsOverviewExporter,
+    ComboCardDataExporter, DataForBursaryExporter,
+    UnpaidPaymentsExporter, SessionPaymentsOverviewExporter,
+    OutstandingCoursesExporter,
+    AccommodationPaymentsExporter,
+    TranscriptDataExporter,
+    TrimmedDataExporter,
+    StudentTrimmedPaymentExporter,
+    get_students,)
+from waeup.kofa.students.accommodation import BedTicket
+from waeup.kofa.students.interfaces import ICSVStudentExporter
+from waeup.kofa.students.payments import StudentOnlinePayment
+from waeup.kofa.students.student import Student
+from waeup.kofa.students.studycourse import StudentStudyCourse
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.kofa.students.tests.test_batching import StudentImportExportSetup
+from waeup.kofa.testing import FunctionalLayer
+
+curr_year = datetime.datetime.now().year
+year_range = range(curr_year - 11, curr_year + 1)
+year_range_str = ','.join([str(i) for i in year_range])
+
+class ExportHelperTests(StudentImportExportSetup):
+    layer = FunctionalLayer
+    def setUp(self):
+        super(ExportHelperTests, self).setUp()
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+        return
+
+    def test_get_students_plain(self):
+        # without a filter we get all students
+        result = get_students(self.app)
+        self.assertEqual(len(list(result)), 1)
+        return
+
+    def test_get_students_by_session(self):
+        # we can filter out students of a certain session
+        my_filter1 = StudentsQuery(current_session=2012)
+        result = get_students(self.app, stud_filter=my_filter1)
+        self.assertEqual(len(list(result)), 1)
+
+        my_filter2 = StudentsQuery(current_session=1964)
+        result = get_students(self.app, stud_filter=my_filter2)
+        self.assertEqual(len(list(result)), 0)
+        return
+
+    def test_get_students_by_level(self):
+        # we can filter out students of a certain level
+        my_filter1 = StudentsQuery(current_level=200)
+        result = get_students(self.app, stud_filter=my_filter1)
+        self.assertEqual(len(list(result)), 1)
+
+        my_filter2 = StudentsQuery(current_level=300)
+        result = get_students(self.app, stud_filter=my_filter2)
+        self.assertEqual(len(list(result)), 0)
+        return
+
+    def test_get_students_by_deptcode(self):
+        # we can filter out students of a certain dept.
+        my_filter1 = StudentsQuery(depcode='NA')
+        result = get_students(self.app, stud_filter=my_filter1)
+        self.assertEqual(len(list(result)), 1)
+
+        my_filter2 = StudentsQuery(depcode='NOTEXISTING')
+        result = get_students(self.app, stud_filter=my_filter2)
+        self.assertEqual(len(list(result)), 0)
+        return
+
+    def test_get_students_by_faccode(self):
+        # we can filter out students of a certain faculty.
+        my_filter1 = StudentsQuery(faccode='NA')
+        result = get_students(self.app, stud_filter=my_filter1)
+        self.assertEqual(len(list(result)), 1)
+
+        my_filter2 = StudentsQuery(faccode='NOTEXISTING')
+        result = get_students(self.app, stud_filter=my_filter2)
+        self.assertEqual(len(list(result)), 0)
+        return
+
+    def test_get_students_by_current_mode(self):
+        # we can filter out students in a certain mode.
+        my_filter1 = StudentsQuery(current_mode='ug_ft')
+        result = get_students(self.app, stud_filter=my_filter1)
+        self.assertEqual(len(list(result)), 1)
+
+        my_filter2 = StudentsQuery(current_mode='NOTEXISTING')
+        result = get_students(self.app, stud_filter=my_filter2)
+        self.assertEqual(len(list(result)), 0)
+        return
+
+
+class StudentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    std_csv_entry = (
+        'my adm code,my clr code,1981-02-04#,anna@sample.com,,'
+        'Anna,,Tester,234,M.,NG,,,"Studentroad 21\nLagos 123456\n",,'
+        '+234-123-12345#,123,f,A111111,0,,,created'
+        )
+
+    def setUp(self):
+        super(StudentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="students")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clr_code,date_of_birth,email,'
+            'employer,firstname,flash_notice,lastname,matric_number,middlename,'
+            'nationality,officer_comment,parents_email,'
+            'perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'password,state,history,'
+            'certcode,is_postgrad,current_level,current_session\r\n'
+            'my adm code,my clr code,'
+            '1981-02-04#,anna@sample.com,,Anna,,Tester,234,M.,NG,,,'
+            '"Studentroad 21\nLagos 123456\n",,+234-123-12345#,123,f,'
+            'A111111,0,,,created'
+            in result
+            )
+        return
+
+
+class StudentTrimmedDataExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    std_csv_entry = (
+        'my adm code,my clr code,1981-02-04#,anna@sample.com,,'
+        'Anna,,Tester,234,M.,NG,,,"Studentroad 21\nLagos 123456\n",,'
+        '+234-123-12345#,123,f,A111111,0,,,created'
+        )
+
+    def setUp(self):
+        super(StudentTrimmedDataExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = TrimmedDataExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, TrimmedDataExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="trimmed")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = TrimmedDataExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,reg_number,firstname,middlename,'
+            'lastname,sex,email,phone,nationality,date_of_birth,state,'
+            'current_mode,certcode,faccode,depcode,current_level,'
+            'current_session,current_verdict\r\n'
+            'A111111,234,123,Anna,M.,Tester,f,anna@sample.com,+234-123-12345#,'
+            'NG,1981-02-04#,created,ug_ft,CERT1,NA,NA,200,2012,0'
+            in result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clr_code,date_of_birth,email,'
+            'employer,firstname,flash_notice,lastname,matric_number,middlename,'
+            'nationality,officer_comment,parents_email,'
+            'perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'password,state,history,certcode,'
+            'is_postgrad,current_level,current_session\r\n'
+            'my adm code,my clr code,1981-02-04#,anna@sample.com,,'
+            'Anna,,Tester,234,M.,NG,,,"Studentroad 21\nLagos 123456\n"'
+            ',,+234-123-12345#,123,f,A111111,0,,,created'
+            in result
+            )
+        return
+
+    def test_export_student(self):
+        # we can export a single student
+        self.setup_student(self.student)
+        exporter = StudentExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clr_code,date_of_birth,email,'
+            'employer,firstname,flash_notice,lastname,matric_number,middlename,'
+            'nationality,officer_comment,parents_email,'
+            'perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'password,state,history,certcode,'
+            'is_postgrad,current_level,current_session\r\n'
+            'my adm code,my clr code,1981-02-04#,anna@sample.com,,'
+            'Anna,,Tester,234,M.,NG,,,"Studentroad 21\nLagos 123456\n"'
+            ',,+234-123-12345#,123,f,A111111,0,,,created'
+            in result
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export a filtered set of students (filtered by session/level)
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentExporter()
+
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None)
+        result1 = open(self.outfile, 'rb').read()
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=2012, current_level=None)
+        result2 = open(self.outfile, 'rb').read()
+        # current_level and current_session can be both a string ...
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session='2012', current_level=u'200')
+        result3 = open(self.outfile, 'rb').read()
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=2011, current_level=None)
+        result4 = open(self.outfile, 'rb').read()
+        # ... and an integer
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=100)
+        result5 = open(self.outfile, 'rb').read()
+        # Also students at probating levels are being exported ...
+        self.student['studycourse'].current_level = 210
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=200)
+        result6 = open(self.outfile, 'rb').read()
+        # ... but not in the wrong level range.
+        self.student['studycourse'].current_level = 310
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=200)
+        result7 = open(self.outfile, 'rb').read()
+        self.assertTrue(self.std_csv_entry in result1)
+        self.assertTrue(self.std_csv_entry in result2)
+        self.assertTrue(self.std_csv_entry in result3)
+        self.assertFalse(self.std_csv_entry in result4)
+        self.assertFalse(self.std_csv_entry in result5)
+        self.assertTrue(self.std_csv_entry in result6)
+        self.assertFalse(self.std_csv_entry in result7)
+        return
+
+    def test_export_selected(self):
+        # we can export a filtered set of students (filtered by session/level)
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentExporter()
+        exporter.export_selected(
+            self.app, self.outfile, selected=['A111111'])
+        result1 = open(self.outfile, 'rb').read()
+        exporter.export_selected(
+            self.app, self.outfile, selected=[])
+        result2 = open(self.outfile, 'rb').read()
+        self.assertTrue(self.std_csv_entry in result1)
+        self.assertFalse(self.std_csv_entry in result2)
+        return
+
+    def test_export_filtered_by_dept(self):
+        # we can export a set of students filtered by department
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentExporter()
+        # current_session can be both a string ...
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session='2012', current_level=u'200', depcode='NA')
+        result1 = open(self.outfile, 'rb').read()
+        # ... and an integer
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=2012, current_level=200, depcode='NODEPT')
+        result2 = open(self.outfile, 'rb').read()
+        self.assertTrue(self.std_csv_entry in result1)
+        self.assertTrue(self.std_csv_entry not in result2)
+        return
+
+    def test_export_filtered_by_faculty(self):
+        # we can export a set of students filtered by faculty
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentExporter()
+
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=2012, current_level='200', faccode='NA')
+        result1 = open(self.outfile, 'rb').read()
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=2012, current_level=200, faccode='NOFAC')
+        result2 = open(self.outfile, 'rb').read()
+        self.assertTrue(self.std_csv_entry in result1)
+        self.assertTrue(self.std_csv_entry not in result2)
+        return
+
+class StudentStudyCourseExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyCourseExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentStudyCourseExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentStudyCourseExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="studentstudycourses")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty study course
+        study_course = StudentStudyCourse()
+        exporter = StudentStudyCourseExporter()
+        exporter.export([study_course], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            ',,,0,,,0,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export study courses.
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        study_course = self.student.get('studycourse')
+        exporter = StudentStudyCourseExporter()
+        exporter.export([study_course], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentStudyCourseExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+    def test_export_student(self):
+        # we can export studycourse of a certain student
+        self.setup_student(self.student)
+        exporter = StudentStudyCourseExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export studycourses of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = StudentStudyCourseExporter()
+        exporter.export_filtered(
+            self.student, self.outfile, current_session=2012)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+    def test_export_selected_student_id(self):
+        # we can export a filtered set of students (filtered by session/level)
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentStudyCourseExporter()
+        exporter.export_selected(
+            self.app, self.outfile, selected=['A111111'])
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+    def test_export_selected_matric_number(self):
+        # we can export a filtered set of students (filtered by session/level)
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentStudyCourseExporter()
+        exporter.export_selected(
+            self.app, self.outfile, selected=['234'])
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'certificate,current_level,current_session,current_verdict,'
+            'entry_mode,entry_session,previous_verdict,student_id\r\n'
+
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
+            )
+        return
+
+
+class StudentStudyLevelExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyLevelExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentStudyLevelExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentStudyLevelExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="studentstudylevels")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty study level
+        study_level = StudentStudyLevel()
+        exporter = StudentStudyLevelExporter()
+        exporter.export([study_level], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,,,0,0,,,,,0,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export study levels.
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        study_course = self.student.get('studycourse')
+        study_level = study_course[study_course.keys()[0]]
+        exporter = StudentStudyLevelExporter()
+        exporter.export([study_level], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,100,2012,A,100,,,,A111111,1,CERT1\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export study levels
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentStudyLevelExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,100,2012,A,100,,,,A111111,1,CERT1\r\n'
+            )
+        return
+
+    def test_export_student(self):
+        # we can really export study levels of a certain student
+        self.setup_student(self.student)
+        exporter = StudentStudyLevelExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,100,2012,A,100,,,,A111111,1,CERT1\r\n'
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export studylevels of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = StudentStudyLevelExporter()
+        exporter.export_filtered(
+            self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,100,2012,A,100,,,,A111111,1,CERT1\r\n'
+            )
+        return
+
+    def test_export_selected(self):
+        # we can export studylevels of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = StudentStudyLevelExporter()
+        exporter.export_selected(
+            self.app, self.outfile, selected=['A111111'])
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'transcript_remark,validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.00,100,2012,A,100,,,,A111111,1,CERT1\r\n'
+            )
+        return
+
+class CourseTicketExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CourseTicketExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CourseTicketExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, CourseTicketExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="coursetickets")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty course ticket
+        ticket = CourseTicket()
+        exporter = CourseTicketExporter()
+        exporter.export([ticket], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '0,0,,,,,,,,0,0,,,,,,,,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export course tickets.
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        study_course = self.student.get('studycourse')
+        study_level = study_course[study_course.keys()[0]]
+        ticket = study_level['CRS1']
+        exporter = CourseTicketExporter()
+        exporter.export([ticket], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all course tickets
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = CourseTicketExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        return
+
+    def test_export_student(self):
+        # we can really export all course tickets of a certain student
+        self.setup_student(self.student)
+        exporter = CourseTicketExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export course tickets of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = CourseTicketExporter()
+        exporter.export_filtered(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        # We can set the course tickets level, semester and level_session 
+        # without code (used in the datacenter)
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(self.student, self.outfile, ct_level='100',
+            ct_session='2012', ct_semester='2')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        # 'all' does select all
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(self.student, self.outfile, ct_level='all',
+            ct_session='2012', ct_semester='all')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        # Level 200 tickets do not exist.
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(self.student, self.outfile, ct_level='200')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+                        )
+        # Session 2013 tickets do not exist.
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(self.student, self.outfile,
+            ct_level='all', ct_session='2013')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            )
+        # 1st semester tickets do not exist.
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(self.student, self.outfile,
+            ct_level='all', ct_session='all', ct_semester='1')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            )
+        # If the coursetickets catalog is used to filter students
+        # and (course) code is not None
+        # only course tickets which belong to this course are exported
+        exporter.export_filtered(
+            self.student, self.outfile, catalog='coursetickets', code='CRS1')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,100,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        exporter.export_filtered(
+            self.student, self.outfile, catalog='coursetickets', code='any code')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            )
+        # Also tickets in probating levels are exported. Therefore
+        # we change the level attribute to fake a 110 level.
+        self.student['studycourse']['100'].level = 110
+        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['CRS1']))
+        exporter.export_filtered(
+            self.student, self.outfile, catalog='coursetickets', code='CRS1', level='100')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,course_category,credits,dcode,fcode,level,level_session,'
+            'mandatory,outstanding,passmark,score,semester,ticket_session,title,student_id,certcode,'
+            'display_fullname\r\n'
+            '1,1,CRS1,,100,DEP1,FAC1,110,2012,0,0,100,,2,,Course 1,A111111,CERT1,'
+            'Anna M. Tester\r\n'
+            )
+        return
+
+class OutstandingCoursesExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(OutstandingCoursesExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = OutstandingCoursesExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, OutstandingCoursesExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get an student exporter as utility
+        result = queryUtility(ICSVExporter, name="outstandingcourses")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_all(self):
+        course1 = Course(u'Cheese Basics', 'C1')
+        course2 = Course(u'Advanced Cheese Making', 'C2')
+        course3 = Course(u'Selling Cheese', 'C3')
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(course1)
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(course2)
+        self.app['faculties']['fac1']['dep1'].courses.addCourse(course3)
+        self.certificate.addCertCourse(course1, 100, True)
+        self.certificate.addCertCourse(course2, 400, False)
+        self.certificate.addCertCourse(course3, 100, False)
+        self.setup_student(self.student)
+        self.student['studycourse']['100']['C3'].score = 25
+        exporter = OutstandingCoursesExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        # The only student has registered C1, C3 and CRS1
+        # She missed C2, has failed C3 and  did not
+        # take C1 and CRS1
+        self.assertEqual(
+            result,
+            'student_id,matric_number,certcode,display_fullname,missed,failed,nottaken\r\n'
+            'A111111,234,CERT1,Anna M. Tester,C2_400 ,C3 ,C1 CRS1 \r\n'
+            )
+        return
+
+class StudentPaymentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentPaymentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentPaymentExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentPaymentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a payments exporter as utility
+        result = queryUtility(ICSVExporter, name="studentpayments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty payment
+        payment = StudentOnlinePayment()
+        payment.creation_date = datetime.datetime(2012, 4, 1, 13, 12, 1)
+        exporter = StudentPaymentExporter()
+        exporter.export([payment], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            ',0.0,2012-04-01 13:12:01#,,[],1,,,,,unpaid,,0.0,,,,,\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export student payments.
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        payment = self.student['payments']['my-payment']
+        exporter = StudentPaymentExporter()
+        exporter.export([payment], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,paid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all payments
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentPaymentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,paid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+    def test_export_student(self):
+        # we can really export all payments of a certain student
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentPaymentExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,paid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export payments of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = StudentPaymentExporter()
+        exporter.export_filtered(
+            self.student, self.outfile, current_level=200)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,paid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+    def test_export_filtered_by_date(self):
+        # payments_start and payments_end are being ignored
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentPaymentExporter()
+        # A key xxx does not exist
+        self.assertRaises(
+            KeyError, exporter.export_filtered, self.app, self.outfile,
+            current_session=None,
+            current_level=None, xxx='nonsense')
+        # payments_start and payments_end do exist but must match format '%Y-%m-%d'
+        self.assertRaises(
+            ValueError, exporter.export_filtered, self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='nonsense', payments_end='nonsense')
+        # If they match the format they are ignored by get_filtered and the
+        # exporter works properly.
+        # Attention: End day is included!
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='01/04/%s' % (curr_year-6),
+            payments_end='01/04/%s' % (curr_year-6))
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,paid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        # Payment date is 2012-04-01, 14:12:01.
+        # No results if payment_date is outside the given period.
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='30/03/2012', payments_end='31/03/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='02/04/2012', payments_end='03/04/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        # No results if payment_date is not set
+        self.payment.payment_date = None
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='31/03/2012', payments_end='02/04/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        return
+
+class StudentTrimmedPaymentExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTrimmedPaymentExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentTrimmedPaymentExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentTrimmedPaymentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a payments exporter as utility
+        result = queryUtility(ICSVExporter, name="trimmedpayments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_all(self):
+        # we can really export all payments
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentTrimmedPaymentExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'amount_auth,creation_date,p_category,p_combi,p_current,'
+            'p_id,p_item,p_level,p_session,p_state,payment_date,'
+            'r_amount_approved,r_code,r_desc,student_id,faccode,'
+            'depcode,state,current_session\r\n'
+
+            '12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,p-item,'
+            '100,%s,paid,%s-04-01 14:12:01#,12.12,r-code,,'
+            'A111111,NA,NA,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+
+class UnpaidPaymentsExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UnpaidPaymentsExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_export_all(self):
+        # we can really export all payments
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = UnpaidPaymentsExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        # No unpaid ticket exists
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        # Make ticket unpaid
+        self.payment.p_state = 'unpaid'
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,'
+            'p-item,100,%s,unpaid,%s-04-01 14:12:01#,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+class BursaryDataExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(BursaryDataExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_export_all(self):
+        # we can really export all payments
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = DataForBursaryExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,p_item,'
+            'p_level,p_session,p_state,payment_date,r_amount_approved,r_code,'
+            'r_desc,student_id,matric_number,reg_number,firstname,middlename,lastname,'
+            'state,current_session,entry_session,entry_mode,faccode,depcode,certcode\r\n'
+
+            '666,12.12,%s-04-01 13:12:01#,schoolfee,[],1,my-id,p-item,100,%s,'
+            'paid,%s-04-01 14:12:01#,12.12,r-code,,A111111,234,123,'
+            'Anna,M.,Tester,created,2012,2010,ug_ft,NA,NA,CERT1\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+class AccommodationPaymentsExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(AccommodationPaymentsExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_export_all(self):
+        self.setup_student(self.student)
+        # add accommodation payments
+        payment = StudentOnlinePayment()
+        payment.creation_date = datetime.datetime(curr_year-6, 4, 1, 13, 12, 1)
+        payment.p_id = 'id1'
+        payment.p_category = u'bed_allocation'
+        payment.p_state = 'paid'
+        payment.ac = u'abc'
+        payment.p_item = u'xyz'
+        payment.p_level = 100
+        payment.p_session = curr_year - 6
+        payment.payment_date = datetime.datetime(curr_year-6, 4, 1, 14, 12, 1)
+        payment.amount_auth = 12.12
+        payment.r_amount_approved = 12.12
+        payment.r_code = u'cde'
+        payment2 = StudentOnlinePayment()
+        payment2.creation_date = datetime.datetime(curr_year-6, 4, 1, 13, 12, 1)
+        payment2.p_id = 'id2'
+        payment2.p_category = u'hostel_maintenance'
+        payment2.p_state = 'paid'
+        payment2.ac = u'abc'
+        payment2.p_item = u'xyz'
+        payment2.p_level = 100
+        payment2.p_session = curr_year - 6
+        payment2.payment_date = datetime.datetime(curr_year-6, 4, 1, 14, 12, 1)
+        payment2.amount_auth = 12.12
+        payment2.r_amount_approved = 12.12
+        payment2.r_code = u'cde'
+        # XXX: there is no addPayment method to give predictable names
+        self.payment = self.student['payments']['id1'] = payment
+        self.payment = self.student['payments']['id2'] = payment2
+        exporter = AccommodationPaymentsExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        # only accommodation payments are exported
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_combi,p_current,p_id,p_item,'
+            'p_level,p_session,p_state,payment_date,r_amount_approved,r_code,'
+            'r_desc,student_id,matric_number,reg_number,firstname,middlename,lastname,'
+            'state,current_session,entry_session,entry_mode,faccode,depcode,certcode\r\n'
+            'abc,12.12,%s-04-01 13:12:01#,bed_allocation,[],1,id1,xyz,100,%s,'
+            'paid,%s-04-01 14:12:01#,12.12,cde,,A111111,234,123,'
+            'Anna,M.,Tester,created,2012,2010,ug_ft,NA,NA,CERT1\r\n'
+            'abc,12.12,%s-04-01 13:12:01#,hostel_maintenance,[],1,id2,xyz,100,%s,'
+            'paid,%s-04-01 14:12:01#,12.12,cde,,A111111,234,123,'
+            'Anna,M.,Tester,created,2012,2010,ug_ft,NA,NA,CERT1\r\n'
+            % (curr_year-6, curr_year-6, curr_year-6,
+               curr_year-6, curr_year-6, curr_year-6)
+            )
+        return
+
+class BedTicketExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(BedTicketExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = BedTicketExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, BedTicketExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a bedtickets exporter as utility
+        result = queryUtility(ICSVExporter, name="bedtickets")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export_empty(self):
+        # we can export a nearly empty bedticket
+        bedticket = BedTicket()
+        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        bedticket.bed = bed
+        exporter = BedTicketExporter()
+        exporter.export([bedticket], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            result,
+            'bed,bed_coordinates,bed_type,booking_code,booking_date,'
+            'booking_session,student_id,actual_bed_type\r\n'
+            'hall-1_A_101_A,,,,<YYYY-MM-DD hh:mm:ss>.<6-DIGITS>#,,,regular_male_fr\r\n'
+            )
+        return
+
+    def test_export(self):
+        # we can really export student bedtickets.
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        bedticket = self.student['accommodation']['2004']
+        exporter = BedTicketExporter()
+        exporter.export([bedticket], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            result,
+            'bed,bed_coordinates,bed_type,booking_code,booking_date,'
+            'booking_session,student_id,actual_bed_type\r\n'
+            'hall-1_A_101_A,,any bed type,,<YYYY-MM-DD hh:mm:ss>.<6-DIGITS>#,2004,'
+            'A111111,regular_male_fr\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export all bedtickets
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = BedTicketExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            result,
+            'bed,bed_coordinates,bed_type,booking_code,booking_date,'
+            'booking_session,student_id,actual_bed_type\r\n'
+            'hall-1_A_101_A,,any bed type,,<YYYY-MM-DD hh:mm:ss>.<6-DIGITS>#,2004,'
+            'A111111,regular_male_fr\r\n'
+            )
+        return
+
+    def test_export_student(self):
+        # we can really export all bedtickets of a certain student
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = BedTicketExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            result,
+            'bed,bed_coordinates,bed_type,booking_code,booking_date,'
+            'booking_session,student_id,actual_bed_type\r\n'
+            'hall-1_A_101_A,,any bed type,,<YYYY-MM-DD hh:mm:ss>.<6-DIGITS>#,2004,'
+            'A111111,regular_male_fr\r\n'
+            )
+        return
+
+    def test_export_filtered(self):
+        # we can export payments of a filtered set of students
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+
+        exporter = BedTicketExporter()
+        exporter.export_filtered(
+            self.student, self.outfile, current_level=200)
+        result = open(self.outfile, 'rb').read()
+        self.assertMatches(
+            result,
+            'bed,bed_coordinates,bed_type,booking_code,booking_date,'
+            'booking_session,student_id,actual_bed_type\r\n'
+            'hall-1_A_101_A,,any bed type,,<YYYY-MM-DD hh:mm:ss>.<6-DIGITS>#,'
+            '2004,A111111,regular_male_fr\r\n')
+        return
+
+
+class SchoolFeePaymentsOverviewExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SchoolFeePaymentsOverviewExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = SchoolFeePaymentsOverviewExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, SchoolFeePaymentsOverviewExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a payments exporter as utility
+        result = queryUtility(ICSVExporter, name="sfpaymentsoverview")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        self.setup_student(self.student)
+        exporter = SchoolFeePaymentsOverviewExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,display_fullname,state,certcode,'
+            'faccode,depcode,is_postgrad,'
+            'current_level,current_session,current_mode,entry_session,'
+            'reg_number,%s\r\n'
+            'A111111,234,Anna M. Tester,created,CERT1,NA,NA,0,'
+            '200,2012,ug_ft,2010,'
+            % year_range_str in result
+            )
+        return
+
+    def test_export_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        # We add successful payments. 
+        payment_2 = StudentOnlinePayment()
+        payment_2.p_id = 'my-id'
+        payment_2.p_session = curr_year - 5
+        payment_2.amount_auth = 13.13
+        payment_2.p_state = 'paid'
+        payment_2.p_category = u'schoolfee'
+        self.student['payments']['my-2ndpayment'] = payment_2
+        # This one could be a balance payment.
+        # The amount is being added.
+        payment_3 = StudentOnlinePayment()
+        payment_3.p_id = 'my-id_2'
+        payment_3.p_session = curr_year - 5
+        payment_3.amount_auth = 1.01
+        payment_3.p_state = 'paid'
+        payment_3.p_category = u'schoolfee'
+        self.student['payments']['my-3rdpayment'] = payment_3
+        # One session school fee has been waived
+        payment_4 = StudentOnlinePayment()
+        payment_4.p_id = 'my-id_2'
+        payment_4.p_session = curr_year - 4
+        payment_4.amount_auth = 1.01
+        payment_4.p_state = 'waived'
+        payment_4.p_category = u'schoolfee'
+        self.student['payments']['my-4thpayment'] = payment_4
+        exporter = SchoolFeePaymentsOverviewExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,display_fullname,state,'
+            'certcode,faccode,depcode,is_postgrad,'
+            'current_level,current_session,current_mode,entry_session,'
+            'reg_number,%s\r\nA111111,234,Anna M. Tester,created,CERT1,NA,NA,0,'
+            '200,2012,ug_ft,2010,123,,,,,,12.12,14.14,waived,,,,\r\n'
+            % year_range_str in result
+            )
+        return
+
+class SessionPaymentsOverviewExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SessionPaymentsOverviewExporterTest, self).setUp()
+        self.setup_for_export()
+        paycats = ('schoolfee', 'clearance', 'gown', 'transcript')
+        paycatyears = ''
+        grok.getSite()['configuration'].current_academic_session = curr_year - 4
+        year_range = range(curr_year-6, curr_year-3)
+        year_range_tuple = tuple([str(year)[2:] for year in year_range])
+        for cat in paycats:
+            for year in year_range_tuple:
+                paycatyears += '%s,'%(cat+str(year))
+        self.paycatyears = paycatyears.strip(',')
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = SessionPaymentsOverviewExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, SessionPaymentsOverviewExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a payments exporter as utility
+        result = queryUtility(ICSVExporter, name="sessionpaymentsoverview")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        self.setup_student(self.student)
+        exporter = SessionPaymentsOverviewExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,display_fullname,state,certcode,'
+            'faccode,depcode,is_postgrad,current_level,current_session,'
+            'current_mode,entry_session,reg_number,%s\r\n'
+            'A111111,234,Anna M. Tester,created,CERT1,NA,NA,0,200,'
+            '2012,ug_ft,2010,123,'
+            '12.12,,,,,,,,,,,\r\n' %self.paycatyears in result
+            )
+        return
+
+    def test_export_all(self):
+        self.setup_student(self.student)
+        # We add successful payments.
+        payment_2 = StudentOnlinePayment()
+        payment_2.p_id = 'my-id'
+        payment_2.p_session = curr_year - 5
+        payment_2.amount_auth = 13.13
+        payment_2.p_state = 'paid'
+        payment_2.p_category = u'schoolfee'
+        self.student['payments']['my-2ndpayment'] = payment_2
+        # This one could be a balance payment.
+        # The amount is being added.
+        payment_3 = StudentOnlinePayment()
+        payment_3.p_id = 'my-id_2'
+        payment_3.p_session = curr_year - 5
+        payment_3.amount_auth = 1.01
+        payment_3.p_state = 'paid'
+        payment_3.p_category = u'schoolfee'
+        self.student['payments']['my-3rdpayment'] = payment_3
+        # One session school fee has been waived
+        payment_4 = StudentOnlinePayment()
+        payment_4.p_id = 'my-id_2'
+        payment_4.p_session = curr_year - 4
+        payment_4.amount_auth = 1.01
+        payment_4.p_state = 'waived'
+        payment_4.p_category = u'schoolfee'
+        self.student['payments']['my-4thpayment'] = payment_4
+        exporter = SessionPaymentsOverviewExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,matric_number,display_fullname,state,certcode,faccode,'
+            'depcode,is_postgrad,current_level,current_session,'
+            'current_mode,entry_session,reg_number,%s\r\n'
+            'A111111,234,Anna M. Tester,created,CERT1,NA,NA,0,200,2012,ug_ft,'
+            '2010,123,'
+            '12.12,14.14,1.01,,,,,,,,,\r\n'
+            %self.paycatyears in result
+            )
+        return
+
+class StudyLevelsOverviewExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudyLevelsOverviewExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        obj = StudyLevelsOverviewExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudyLevelsOverviewExporter)
+        return
+
+    def test_get_as_utility(self):
+        result = queryUtility(ICSVExporter, name="studylevelsoverview")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        self.setup_student(self.student)
+        exporter = StudyLevelsOverviewExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+             'student_id,state,certcode,faccode,depcode,is_postgrad,'
+             'entry_session,current_level,current_session,'
+             '0,10,100,110,120,200,210,220,300,310,320,400,410,420,500,'
+             '510,520,600,610,620,700,710,720,800,810,820,900,910,920,999\r\n'
+             'A111111,created,CERT1,NA,NA,0,2010,200,2012,,,2012'
+             ',,,,,,,,,,,,,,,,,,,,,,,,,,,\r\n',
+            result
+            )
+        return
+
+    def test_export_all(self):
+        self.setup_student(self.student)
+        exporter = StudyLevelsOverviewExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            'student_id,state,certcode,faccode,depcode,is_postgrad,'
+            'entry_session,current_level,current_session,'
+            '0,10,100,110,120,200,210,220,300,310,320,400,410,420,500,'
+            '510,520,600,610,620,700,710,720,800,810,820,900,910,920,999\r\n'
+            'A111111,created,CERT1,NA,NA,0,2010,200,2012,,,2012'
+            ',,,,,,,,,,,,,,,,,,,,,,,,,,,\r\n',
+            result
+            )
+        return
+
+class ComboCardExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ComboCardExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def create_passport_img(self, student):
+        # create some passport file for `student`
+        storage = getUtility(IExtFileStore)
+        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+        self.image_contents = open(image_path, 'rb').read()
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr='passport.jpg')
+        storage.createFile(file_id, StringIO(self.image_contents))
+
+    def test_export_all(self):
+        self.setup_student(self.student)
+        self.create_passport_img(self.student)
+        exporter = ComboCardDataExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'display_fullname,student_id,matric_number,certificate,faculty,'
+            'department,passport_path\r\nAnna M. Tester,A111111,234,'
+            'Unnamed Certificate,Faculty of Unnamed Faculty (NA),'
+            'Department of Unnamed Department (NA),'
+            'students/00110/A111111/passport_A111111.jpg\r\n'
+            in result
+            )
+        return
+
+class TranscriptDataExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(TranscriptDataExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_export_all(self):
+        self.setup_student(self.student)
+        exporter = TranscriptDataExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'student_id,state,certcode,faccode,depcode,entry_session,'
+            'current_level,current_session,transcript_data\r\n'
+            'A111111,created,CERT1,NA,NA,2010,200,2012,'
+            'Level 100; 1st: ; 2nd: CRS1; 3rd: ; sgpa: 0.0\r\n'
+            in result
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_payment.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_payment.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_payment.py	(revision 16045)
@@ -0,0 +1,52 @@
+## $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
+##
+"""Tests for student payments.
+
+"""
+
+from zope.interface import verify
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.students.payments import (
+    StudentOnlinePayment, StudentOnlinePaymentFactory, Payer)
+from waeup.kofa.payments.interfaces import IOnlinePayment, IPayer
+from waeup.kofa.students.interfaces import IStudentNavigation
+
+class StudentOnlinePaymentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentOnlinePaymentFactoryTest, self).setUp()
+        self.factory = StudentOnlinePaymentFactory()
+        return
+
+    def tearDown(self):
+        super(StudentOnlinePaymentFactoryTest, self).tearDown()
+        return
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, StudentOnlinePayment)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IOnlinePayment)
+        assert implemented_by.isOrExtends(IStudentNavigation)
+
+    def test_payer_interface(self):
+        verify.verifyClass(IPayer, Payer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_student.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_student.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_student.py	(revision 16045)
@@ -0,0 +1,427 @@
+## $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
+##
+"""Tests for students and related.
+"""
+import os
+import re
+import unittest
+import grok
+from cStringIO import StringIO
+from datetime import tzinfo
+from zope.component import getUtility, queryUtility, createObject
+from zope.catalog.interfaces import ICatalog
+from zope.component.interfaces import IFactory
+from zope.event import notify
+from zope.interface import verify
+from zope.schema.interfaces import RequiredMissing
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.students.student import (
+    Student, StudentFactory, handle_student_removed, path_from_studid)
+from waeup.kofa.students.studycourse import StudentStudyCourse
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.kofa.students.payments import StudentPaymentsContainer
+from waeup.kofa.students.accommodation import StudentAccommodation, BedTicket
+from waeup.kofa.students.interfaces import (
+    IStudent, IStudentStudyCourse, IStudentPaymentsContainer,
+    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, IBedTicket,
+    IStudentNavigation, IStudentsUtils)
+from waeup.kofa.students.tests.test_batching import StudentImportExportSetup
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.university.department import Department
+
+class HelperTests(unittest.TestCase):
+    # Tests for helper functions in student module.
+
+    def test_path_from_studid(self):
+        # make sure we get predictable paths from student ids.
+        self.assertEqual(
+            path_from_studid('K1000000'), u'01000/K1000000')
+        self.assertEqual(
+            path_from_studid('K1234567'), u'01234/K1234567')
+        self.assertEqual(
+            path_from_studid('K12345678'), u'12345/K12345678')
+        # The algorithm works also for overlong numbers, just to be
+        # sure.
+        self.assertEqual(
+            path_from_studid('K123456789'), u'123456/K123456789')
+        # low numbers (< 10**6) are treated special: they get max. of
+        # 10,000 entries. That's mainly because of old students
+        # migrated into our portal.
+        self.assertEqual(
+            path_from_studid('KM123456'), u'00120/KM123456')
+        return
+
+class StudentTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTest, self).setUp()
+        self.student = Student()
+        self.student.firstname = u'Anna'
+        self.student.lastname = u'Tester'
+        self.studycourse = StudentStudyCourse()
+        self.studylevel = StudentStudyLevel()
+        self.studylevel.level_session = 2015
+        self.courseticket = CourseTicket()
+        self.payments = StudentPaymentsContainer()
+        self.accommodation = StudentAccommodation()
+        self.bedticket = BedTicket()
+        return
+
+    def tearDown(self):
+        super(StudentTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IStudent, Student)
+        verify.verifyClass(IStudentNavigation, Student)
+        verify.verifyObject(IStudent, self.student)
+        verify.verifyObject(IStudentNavigation, self.student)
+
+        verify.verifyClass(IStudentStudyCourse, StudentStudyCourse)
+        verify.verifyClass(IStudentNavigation, StudentStudyCourse)
+        verify.verifyObject(IStudentStudyCourse, self.studycourse)
+        verify.verifyObject(IStudentNavigation, self.studycourse)
+
+        verify.verifyClass(IStudentStudyLevel, StudentStudyLevel)
+        verify.verifyClass(IStudentNavigation, StudentStudyLevel)
+        verify.verifyObject(IStudentStudyLevel, self.studylevel)
+        verify.verifyObject(IStudentNavigation, self.studylevel)
+
+        verify.verifyClass(ICourseTicket, CourseTicket)
+        verify.verifyClass(IStudentNavigation, CourseTicket)
+        verify.verifyObject(ICourseTicket, self.courseticket)
+        verify.verifyObject(IStudentNavigation, self.courseticket)
+
+        verify.verifyClass(IStudentPaymentsContainer, StudentPaymentsContainer)
+        verify.verifyClass(IStudentNavigation, StudentPaymentsContainer)
+        verify.verifyObject(IStudentPaymentsContainer, self.payments)
+        verify.verifyObject(IStudentNavigation, self.payments)
+
+        verify.verifyClass(IStudentAccommodation, StudentAccommodation)
+        verify.verifyClass(IStudentNavigation, StudentAccommodation)
+        verify.verifyObject(IStudentAccommodation, self.accommodation)
+        verify.verifyObject(IStudentNavigation, self.accommodation)
+
+        verify.verifyClass(IBedTicket, BedTicket)
+        verify.verifyClass(IStudentNavigation, BedTicket)
+        verify.verifyObject(IBedTicket, self.bedticket)
+        verify.verifyObject(IStudentNavigation, self.bedticket)
+        return
+
+    def test_base(self):
+        department = Department()
+        studycourse = StudentStudyCourse()
+        self.assertRaises(
+            TypeError, studycourse.addStudentStudyLevel, department)
+        studylevel = StudentStudyLevel()
+        self.assertRaises(
+            TypeError, studylevel.addCourseTicket, department, department)
+
+    def test_booking_date(self):
+        isinstance(self.bedticket.booking_date.tzinfo, tzinfo)
+        self.assertEqual(self.bedticket.booking_date.tzinfo, None)
+        return
+
+    def test_maint_payment_made(self):
+        self.assertFalse(self.bedticket.maint_payment_made)
+        return
+
+
+class StudentRemovalTests(StudentImportExportSetup):
+    # Test handle_student_removed
+    #
+    # This is a complex action updating several CSV files and moving
+    # stored files to a backup location.
+    #
+    # These tests make no assumptions about the CSV files except that
+    # they contain a deletion timestamp at end of each data row
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentRemovalTests, self).setUp()
+        self.setup_for_export()
+        return
+
+    def create_passport_img(self, student):
+        # create some passport file for `student`
+        storage = getUtility(IExtFileStore)
+        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+        self.image_contents = open(image_path, 'rb').read()
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr='passport.jpg')
+        storage.createFile(file_id, StringIO(self.image_contents))
+
+    def test_backup_single_student_data(self):
+        # when a single student is removed, the data is backed up.
+        self.setup_student(self.student)
+        # Add a fake image
+        self.create_passport_img(self.student)
+        handle_student_removed(self.student, None)
+        del_dir = self.app['datacenter'].deleted_path
+        del_img_path = os.path.join(
+            del_dir, 'media', 'students', '00110', 'A111111',
+            'passport_A111111.jpg')
+        # The image was copied over
+        self.assertTrue(os.path.isfile(del_img_path))
+        self.assertEqual(
+            open(del_img_path, 'rb').read(),
+            self.image_contents)
+        # The student data were put into CSV files
+        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
+            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
+        for name in STUDENT_BACKUP_EXPORTER_NAMES:
+            csv_path = os.path.join(del_dir, '%s.csv' % name)
+            self.assertTrue(os.path.isfile(csv_path))
+            contents = open(csv_path, 'rb').read().split('\r\n')
+            # We expect 3 lines output including a linebreak at end of file.
+            self.assertEqual(len(contents), 3)
+        return
+
+    def test_backup_single_graduated_data(self):
+        # when a single graduated student is removed, the data is backed up
+        # somewhere else
+        self.setup_student(self.student)
+        IWorkflowState(self.student).setState('graduated')
+        # Add a fake image
+        self.create_passport_img(self.student)
+        handle_student_removed(self.student, None)
+        del_dir = self.app['datacenter'].graduated_path
+        del_img_path = os.path.join(
+            del_dir, 'media', 'students', '00110', 'A111111',
+            'passport_A111111.jpg')
+        # The image was copied over
+        self.assertTrue(os.path.isfile(del_img_path))
+        self.assertEqual(
+            open(del_img_path, 'rb').read(),
+            self.image_contents)
+        # The student data were put into CSV files
+        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
+            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
+        for name in STUDENT_BACKUP_EXPORTER_NAMES:
+            csv_path = os.path.join(del_dir, '%s.csv' % name)
+            self.assertTrue(os.path.isfile(csv_path))
+            contents = open(csv_path, 'rb').read().split('\r\n')
+            # We expect 3 lines output including a linebreak at end of file.
+            self.assertEqual(len(contents), 3)
+        return
+
+    def test_backup_append_csv(self):
+        # when several students are removed, existing CSVs are appended
+        self.setup_student(self.student)
+        # Add a fake image
+        self.create_passport_img(self.student)
+        del_dir = self.app['datacenter'].deleted_path
+        # put fake data into students.csv with trailing linebreak
+        students_csv = os.path.join(del_dir, 'students.csv')
+        open(students_csv, 'wb').write('line1\r\nline2\r\n')
+        handle_student_removed(self.student, None)
+        contents = open(students_csv, 'rb').read().split('\r\n')
+        # there should be 4 lines in result csv (including trailing linebreak)
+        self.assertEqual(len(contents), 4)
+        return
+
+    def test_old_files_removed(self):
+        # make sure old files are not accessible any more
+        self.setup_student(self.student)
+        # Add a fake image
+        self.create_passport_img(self.student)
+        # make sure we can access the image before removal
+        file_store = getUtility(IExtFileStore)
+        image = file_store.getFileByContext(self.student, attr='passport.jpg')
+        self.assertTrue(image is not None)
+
+        # remove image (hopefully)
+        handle_student_removed(self.student, None)
+
+        # the is not accessible anymore
+        image = file_store.getFileByContext(self.student, attr='passport.jpg')
+        self.assertEqual(image, None)
+        return
+
+    def test_csv_file_entries_have_timestamp(self):
+        # each row in written csv files has a ``del_date`` column to
+        # tell when the associated student was deleted
+        self.setup_student(self.student)
+        del_dir = self.app['datacenter'].deleted_path
+        students_csv = os.path.join(del_dir, 'students.csv')
+        handle_student_removed(self.student, None)
+        contents = open(students_csv, 'rb').read().split('\r\n')
+        # the CSV header ends with a ``del_date`` column
+        self.assertTrue(contents[0].endswith(',del_date'))
+        # each line ends with an UTC timestamp
+        timestamp = contents[1][-23:]
+        self.assertTrue(re.match(
+            '^\d\d-\d\d-\d\d \d\d:\d\d:\d\d\+00:00$', timestamp))
+        return
+
+class StudentTransferTests(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTransferTests, self).setUp()
+
+        # Add additional certificate
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = 'CERT2'
+        self.certificate2.application_category = 'basic'
+        self.certificate2.start_level = 200
+        self.certificate2.end_level = 500
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add student with subobjects
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+        return
+
+    def test_transfer_student(self):
+        self.assertRaises(
+            RequiredMissing, self.student.transfer, self.certificate2)
+        error = self.student.transfer(self.certificate2, current_session=1000)
+        self.assertTrue(error == -1)
+        error = self.student.transfer(self.certificate2, current_session=2013)
+        self.assertTrue(error == None)
+        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
+        self.assertEqual(self.student['studycourse_1'].current_session, 2012)
+        self.assertEqual(self.student['studycourse'].current_session, 2013)
+        self.assertEqual(self.student['studycourse'].entry_session,
+            self.student['studycourse_1'].entry_session)
+        self.assertEqual(self.student['studycourse_1'].__name__, 'studycourse_1')
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - K1000000 - transferred from CERT1 to CERT2'
+            in logcontent)
+        messages = ' '.join(self.student.history.messages)
+        self.assertMatches(
+            '...<YYYY-MM-DD hh:mm:ss> UTC - '
+            'Transferred from CERT1 to CERT2 by system', messages)
+
+        # The students_catalog has been updated.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 0)
+        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+        results = cat.searchResults(current_session=(2013,2013))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+
+        # studycourse_1 is the previous course.
+        self.assertFalse(self.student['studycourse'].is_previous)
+        self.assertTrue(self.student['studycourse_1'].is_previous)
+
+        # Students can be transferred (only) two times.
+        error = self.student.transfer(self.certificate,
+            current_session=2013)
+        self.assertTrue(error == None)
+        error = self.student.transfer(self.certificate2,
+            current_session=2013)
+        self.assertTrue(error == -3)
+        self.assertEqual([i for i in self.student.keys()],
+            [u'accommodation', u'payments', u'studycourse',
+             u'studycourse_1', u'studycourse_2'])
+
+        # The studycourse with highest order number is the previous
+        # course.
+        self.assertFalse(self.student['studycourse'].is_previous)
+        self.assertFalse(self.student['studycourse_1'].is_previous)
+        self.assertTrue(self.student['studycourse_2'].is_previous)
+
+        # The students_catalog has been updated again.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+
+        # Previous transfer can be successively reverted.
+        self.assertEqual(self.student['studycourse_2'].certificate.code, 'CERT2')
+        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
+        error = self.student.revert_transfer()
+        self.assertTrue(error == None)
+        self.assertEqual([i for i in self.student.keys()],
+            [u'accommodation', u'payments', u'studycourse',
+             u'studycourse_1'])
+        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
+        # The students_catalog has been updated again.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+        error = self.student.revert_transfer()
+        self.assertTrue(error == None)
+        self.assertEqual([i for i in self.student.keys()],
+            [u'accommodation', u'payments', u'studycourse'])
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
+        error = self.student.revert_transfer()
+        self.assertTrue(error == -1)
+        # The students_catalog has been updated again.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 0)
+        logcontent = open(logfile).read()
+        self.assertTrue('system - K1000000 - transfer reverted'
+            in logcontent)
+        messages = ' '.join(self.student.history.messages)
+        self.assertTrue('Transfer reverted by system' in messages)
+        return
+
+class StudentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentFactoryTest, self).setUp()
+        self.factory = StudentFactory()
+
+    def tearDown(self):
+        super(StudentFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, StudentFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, Student)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(IStudent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_utils.py	(revision 16045)
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+import unittest
+import grok
+from zope.component import getUtility, queryUtility, createObject
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.utils import formatted_text
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+
+class FormatterTests(unittest.TestCase):
+
+    def test_formatted_text(self):
+        # we can format strings, unicode and things convertable to unicode.
+        result1 = formatted_text('sample')
+        result2 = formatted_text('ümlaut')
+        result3 = formatted_text(3)
+        result4 = formatted_text(u'unicöde')
+        result5 = formatted_text(u'sample', color='red')
+        self.assertTrue(isinstance(result1, unicode))
+        self.assertEqual(
+            result1, u'<font color="black">sample</font>')
+        self.assertEqual(
+            result2, u'<font color="black">ümlaut</font>')
+        self.assertEqual(
+            result3, u'<font color="black">3</font>')
+        self.assertEqual(
+            result4, u'<font color="black">unicöde</font>')
+        self.assertEqual(
+            result5, u'<font color="red">sample</font>')
+        return
+
+class StudentsUtilsTests(StudentsFullSetup):
+
+    def test_setReturningData(self):
+        utils = getUtility(IStudentsUtils)
+        self.student['studycourse'].current_level = 600
+        utils.setReturningData(self.student)
+        # The new level exceeds the certificates end_level.
+        # In this case current_level remains unchanged and no error is raised.
+        self.assertEqual(self.student['studycourse'].current_level, 600)
+
+    def test_setMatricNumber(self):
+        site = grok.getSite()
+        utils = getUtility(IStudentsUtils)
+        # Matric number can't be set twice.
+        msg, mnumber = utils.setMatricNumber(self.student)
+        self.assertEqual(msg, 'Matriculation number already set.')
+        self.assertEqual(mnumber, None)
+        self.assertEqual(self.student.matric_number, '234')
+        self.student.matric_number = None
+        # Matric number can't be set if next_matric_integer is 0.
+        msg, mnumber = utils.setMatricNumber(self.student)
+        self.assertEqual(msg, 'Matriculation number cannot be set.')
+        self.assertEqual(mnumber, None)
+        site['configuration'].next_matric_integer = 1
+        # Now all requirements are met and matric number can be set.
+        msg, mnumber = utils.setMatricNumber(self.student)
+        self.assertEqual(msg, None)
+        self.assertEqual(mnumber, '1')
+        self.assertEqual(self.student.matric_number, '1')
+        self.assertEqual(site['configuration'].next_matric_integer, 2)
+        # Student can be found in catalog with new matric number.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(cat.searchResults(matric_number=('1', '1')))
+        self.assertEqual(self.student,results[0])
+        # Add another student.
+        another_student = createObject('waeup.Student')
+        another_student.matric_number = u'2'
+        self.app['students'].addStudent(another_student)
+        # Matric number can't be assigned twice.
+        self.student.matric_number = None
+        msg, mnumber = utils.setMatricNumber(self.student)
+        self.assertEqual(msg, 'Matriculation number 2 exists.')
+        self.assertEqual(mnumber, None)
+        self.assertEqual(self.student.matric_number, None)
+        # Certificate must be set.
+        self.student.matric_number = None
+        another_student.matric_number = u'999'
+        self.student['studycourse'].certificate = None
+        msg, mnumber = utils.setMatricNumber(self.student)
+        self.assertEqual(msg, 'No certificate assigned.')
+        return
+
+    def test_set_payment_details(self):
+        self.app['configuration']['2004'].gown_fee = 150.0
+        self.app['configuration']['2004'].transcript_fee = 90.0
+        self.app['configuration']['2004'].late_registration_fee = 180.0
+        utils = getUtility(IStudentsUtils)
+        error, payment = utils.setPaymentDetails(
+            'combi',self.student, None, None,
+            ['gown', 'transcript', 'late_registration'])
+        self.assertEqual(payment.p_level, 100)
+        self.assertEqual(payment.p_session, 2004)
+        self.assertEqual(payment.amount_auth, 420.0)
+        self.assertEqual(payment.p_item,
+            u'Gown Hire Fee + Transcript Fee + Late Course Registration Fee')
+        self.assertEqual(payment.p_combi,
+            ['gown', 'transcript', 'late_registration'])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_vocabularies.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_vocabularies.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_vocabularies.py	(revision 16045)
@@ -0,0 +1,123 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+import unittest
+from waeup.kofa.students.vocabularies import (
+    StudyLevelSource, levels_from_range,
+    )
+from waeup.kofa.university.certificate import Certificate
+
+class HelperTests(unittest.TestCase):
+
+    def test_levels_from_range(self):
+        # default
+        self.assertEqual(
+            levels_from_range(),
+            [10,
+             100, 110, 120, 200, 210, 220, 300, 310, 320, 400, 410, 420,
+             500, 510, 520, 600, 610, 620, 700, 710, 720, 800, 810, 820,
+             900, 910, 920, 999,])
+        # edge-case: level 10
+        self.assertEqual(
+            levels_from_range(10, 10), [10])
+        # edge-case: level 999
+        self.assertEqual(
+            levels_from_range(999, 10), [999])
+        self.assertEqual(
+            levels_from_range(10, 999), [999])
+        self.assertEqual(
+            levels_from_range(10, 200),
+            [10, 100, 110, 120, 200, 210, 220, 300, 310, 320])
+        self.assertEqual(
+            levels_from_range(100, 300),
+            [100, 110, 120, 200, 210, 220, 300, 310, 320, 400, 410, 420])
+        self.assertEqual(
+            levels_from_range(800, 900),
+            [800, 810, 820, 900, 910, 920])
+        return
+
+class FakeStudyCourse(object):
+
+    def __init__(self):
+        cert = Certificate(code="CERT1")
+        cert.start_level = 100
+        cert.end_level = 500
+        self.certificate = cert
+
+class VocabularyTests(unittest.TestCase):
+
+    def test_studylevelsource(self):
+        studycourse = FakeStudyCourse()
+        studylevelsource = StudyLevelSource().factory
+        values = studylevelsource.getValues(studycourse)
+        self.assertEqual(values, [0, 100, 110, 120, 200, 210, 220, 300, 310, 320,
+            400, 410, 420, 500, 510, 520, 600, 610, 620])
+        # All titles do exist
+        titles = [studylevelsource.getTitle(studycourse, value)
+            for value in values]
+        self.assertEqual(len(titles), 19)
+        # Unfortunately, unittests don't know about internationalization
+        self.assertEqual(studylevelsource.getTitle(studycourse, None),
+            'Error: level id ${value} out of range')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 0),
+            'Level Zero')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 1),
+            'Error: level id ${value} out of range')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 10),
+            'Error: level id ${value} out of range')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 100),
+            '100 (Year 1)')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 110),
+            '${title} on 1st probation')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 120),
+            '${title} on 2nd probation')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 500),
+            '500 (Year 5)')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 600),
+            '${title} 1st spillover')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 610),
+            '${title} 2nd spillover')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 620),
+            '${title} 3rd spillover')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 630),
+            'Error: level id ${value} out of range')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 700),
+            'Error: level id ${value} out of range')
+        # Now we modify the certificates to be a pure pre-studies
+        # course
+        studycourse.certificate.start_level = 10
+        studycourse.certificate.end_level = 10
+        values = studylevelsource.getValues(studycourse)
+        self.assertEqual(values, [0, 10])
+        titles = [studylevelsource.getTitle(studycourse, value)
+            for value in values]
+        self.assertEqual(len(titles), 2)
+        self.assertEqual(studylevelsource.getTitle(studycourse, 10),
+            'Pre-Studies')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 100),
+            'Error: level id ${value} out of range')
+        self.assertEqual(studylevelsource.getTitle(studycourse, 200),
+            'Error: level id ${value} out of range')
+        # Finally we modify the certificate to be a mixed course,
+        # starting with pre-studies.
+        studycourse.certificate.end_level = 200
+        values = studylevelsource.getValues(studycourse)
+        self.assertEqual(values, [0, 10, 100, 110, 120,
+            200, 210, 220, 300, 310, 320])
+        titles = [studylevelsource.getTitle(studycourse, value)
+            for value in values]
+        self.assertEqual(len(titles), 11)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_webservices.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_webservices.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/tests/test_webservices.py	(revision 16045)
@@ -0,0 +1,763 @@
+# Tests for webservices
+import xmlrpclib
+import os
+from cStringIO import StringIO
+from zope.app.testing.xmlrpc import ServerProxy
+from zope.component import getUtility
+from waeup.kofa.interfaces import (
+    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.students.payments import StudentOnlinePayment
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+
+
+class XMLRPCTests(StudentsFullSetup):
+    # check XMLRPC services for university portal
+
+    layer = FunctionalLayer
+
+    def setup_student(self, student):
+        study_level = StudentStudyLevel()
+        study_level.level_session = 2012
+        study_level.level_verdict = "A"
+        study_level.level = 100
+        study_level.validated_by = u"my adviser"
+        student['studycourse'].addStudentStudyLevel(
+            self.certificate, study_level)
+
+        ticket = CourseTicket()
+        ticket.automatic = True
+        ticket.carry_over = True
+        ticket.code = u'CRS1'
+        ticket.title = u'Course 1'
+        ticket.fcode = u'FAC1'
+        ticket.dcode = u'DEP1'
+        ticket.credits = 100
+        ticket.passmark = 100
+        ticket.semester = 2
+        study_level[ticket.code] = ticket
+
+    def create_passport_img(self, student):
+        # create some passport file for `student`
+        storage = getUtility(IExtFileStore)
+        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
+        self.image_contents = open(image_path, 'rb').read()
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr='passport.jpg')
+        storage.createFile(file_id, StringIO(self.image_contents))
+
+    def create_fpm_file(self, student, finger_num):
+        # create some .fpm file for `student` finger `finger_num`
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr='%s.fpm' % finger_num)
+        storage.createFile(file_id, StringIO('FP1FakedMintiaeFile1'))
+
+    def XMLRPC_post(self, body):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.addHeader('Content-Length', len(body))
+        self.browser.post('http://localhost/app', body,
+            'text/xml; charset=utf-8')
+        return self.browser.contents
+
+    def test_get_student_id_no_match(self):
+        # w/o any students we get none
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        result = server.get_student_id('Nonsense')
+        self.assertTrue(result is None)
+        return
+
+    def test_get_student_id_regno_exists(self):
+        # we can get the id of an existing student with matching reg_no
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        result = server.get_student_id('123')
+        self.assertEqual(result, 'K1000000')
+        self.assertEqual(self.student_id, result)
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_student_id</methodName>
+<params>
+<param>
+<value><string>123</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_get_student_id_block_unauthorized(self):
+        # requests from unauthorized users are blocked
+        # no username nor password
+        server = ServerProxy('http://localhost/app')
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, '123')
+        # wrong password
+        server = ServerProxy('http://mgr:WRONGPW@localhost/app')
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, '123')
+        # wrong username
+        server = ServerProxy('http://WRONGUSER:mgrpw@localhost/app')
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, '123')
+        return
+
+    def test_get_courses_by_session(self):
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        result = server.get_courses_by_session('K1000000')
+        self.assertEqual(result, None)
+        self.setup_student(self.student)
+        result = server.get_courses_by_session('K1000000', '2010')
+        self.assertEqual(result, None)
+        result = server.get_courses_by_session('K1000000', '2012')
+        self.assertEqual(result,
+            {'100|CRS1': 'Course 1', '100|COURSE1': 'Unnamed Course'})
+        result = server.get_courses_by_session('K1000000')
+        self.assertEqual(result,
+            {'100|CRS1': 'Course 1', '100|COURSE1': 'Unnamed Course'})
+        # Also matric_number ...
+        result = server.get_courses_by_session('234')
+        self.assertEqual(result,
+            {'100|CRS1': 'Course 1', '100|COURSE1': 'Unnamed Course'})
+        # ... or reg_number can be used.
+        result = server.get_courses_by_session('123')
+        self.assertEqual(result,
+            {'100|CRS1': 'Course 1', '100|COURSE1': 'Unnamed Course'})
+        result = server.get_courses_by_session('Nonsense')
+        self.assertEqual(result, None)
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_courses_by_session</methodName>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>100|CRS1</name>
+<value><string>Course 1</string></value>
+</member>
+<member>
+<name>100|COURSE1</name>
+<value><string>Unnamed Course</string></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_get_students_by_course(self):
+        self.setup_student(self.student)
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        result = server.get_students_by_course('CRS1', '2010')
+        self.assertEqual(result, None)
+        result = server.get_students_by_course('CRS1', '2012')
+        self.assertEqual(result, [['K1000000', '234', 'my adviser', 0], ])
+        result = server.get_students_by_course('CRS1')
+        self.assertEqual(result, [['K1000000', '234', 'my adviser', 0], ])
+        payment = StudentOnlinePayment()
+        payment.p_id = 'my-id'
+        payment.p_session = 2012
+        payment.amount_auth = 12.12
+        payment.p_state = u'paid'
+        payment.p_category = u'schoolfee'
+        self.student['payments']['my-payment'] = payment
+        result = server.get_students_by_course('CRS1')
+        self.assertEqual(result, [['K1000000', '234', 'my adviser', 12.12], ])
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_students_by_course</methodName>
+<params>
+<param>
+<value><string>CRS1</string></value>
+<value><string>2012</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><array><data>
+<value><array><data>
+<value><string>K1000000</string></value>
+<value><string>234</string></value>
+<value><string>my adviser</string></value>
+<value><double>12.12</double></value>
+</data></array></value>
+</data></array></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_get_student_info(self):
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        result = server.get_student_info('123')
+        self.assertEqual(result,
+            ['Anna Tester', 'CERT1', '1234', 'aa@aa.ng'])
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_student_info</methodName>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><array><data>
+<value><string>Anna Tester</string></value>
+<value><string>CERT1</string></value>
+<value><string>1234</string></value>
+<value><string>aa@aa.ng</string></value>
+</data></array></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_get_student_passport(self):
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.create_passport_img(self.student)
+        result = server.get_student_passport('123')
+        img = getUtility(IExtFileStore).getFileByContext(
+            self.student, attr='passport.jpg')
+        binary = img.read()
+        self.assertEqual(binary, result)
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_student_passport</methodName>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><base64>
+/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4ADkFkb2JlAGTAAAAAAf/b
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertTrue(xmlout.startswith(RESPONSE_XML))
+
+    def test_get_paid_sessions(self):
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        payment = StudentOnlinePayment()
+        payment.p_id = 'my-id'
+        payment.p_session = 2009
+        payment.amount_auth = 12.12
+        payment.p_state = u'paid'
+        payment.p_category = u'schoolfee'
+        self.student['payments']['my-payment'] = payment
+        result = server.get_paid_sessions('123')
+        self.assertEqual(result, {'2009': 12.12})
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_paid_sessions</methodName>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>2009</name>
+<value><double>12.12</double></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_check_student_credentials(self):
+        # make sure we can get student infos providing valid creds
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        stud_id = self.student.student_id
+        result = server.check_student_credentials(stud_id, 'spwd')
+        self.assertEqual(
+            result, {
+                'description': 'Anna Tester',
+                'email': 'aa@aa.ng',
+                'id': 'K1000000',
+                'type': 'student'}
+            )
+        return
+
+    def test_get_student_moodle_data(self):
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        result = server.get_student_moodle_data(self.student.student_id)
+        self.assertEqual(result,
+            {'lastname': 'Tester', 'email': 'aa@aa.ng', 'firstname': 'Anna'})
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_student_moodle_data</methodName>
+<params>
+<param>
+<value><string>K1000000</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><struct>
+<member>
+<name>lastname</name>
+<value><string>Tester</string></value>
+</member>
+<member>
+<name>email</name>
+<value><string>aa@aa.ng</string></value>
+</member>
+<member>
+<name>firstname</name>
+<value><string>Anna</string></value>
+</member>
+</struct></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        self.assertEqual(xmlout, RESPONSE_XML)
+        return
+
+    def test_put_student_fingerprints_no_stud(self):
+        # invalid student ids will result in `False`
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.assertRaises(
+            xmlrpclib.Fault, server.put_student_fingerprints,
+            'invalid id', {})
+
+    def test_put_student_fingerprints_non_dict(self):
+        # fingerprints must be passed in in a dict
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.assertRaises(
+            xmlrpclib.Fault, server.put_student_fingerprints,
+            self.student.student_id, 'not-a-dict')
+
+    def test_put_student_fingerprints_non_num_keys_ignored(self):
+        # non-numeric keys in fingerprint dict are ignored
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        result = server.put_student_fingerprints(
+            self.student.student_id, {'not-a-num': 'foo',
+                                      '12.2': 'bar',
+                                      '123': 'baz'})
+        self.assertEqual(result, False)
+
+    def test_put_student_fingerprints_non_fpm_data(self):
+        # we cannot pass non-.fpm files as values
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.assertRaises(
+            xmlrpclib.Fault, server.put_student_fingerprints,
+            self.student.student_id, {'1': 'not-a-fingerprint'})
+
+    def test_put_student_fingerprints_invalid_file_format(self):
+        # invalid files will result in `False`
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        invalid_fpm = xmlrpclib.Binary('invalid file')
+        self.assertRaises(
+            xmlrpclib.Fault, server.put_student_fingerprints,
+            self.student.student_id, {'1': invalid_fpm})
+
+    def test_put_student_fingerprints(self):
+        # we can store fingerprints
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        fpm = xmlrpclib.Binary('FP1faked_fpm')
+        result = server.put_student_fingerprints(
+            self.student.student_id, {'1': fpm})
+        self.assertEqual(result, True)
+        stored_file = getUtility(IExtFileStore).getFileByContext(
+            self.student, attr="finger1.fpm")
+        self.assertEqual(stored_file.read(), 'FP1faked_fpm')
+        # storing is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - grok.meta.StudentsXMLRPC '
+            '- K1000000 - fingerprint stored' in logcontent)
+
+    def test_put_student_fingerprints_existing(self):
+        # existing fingerprints are overwritten
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.create_fpm_file(self.student, '1')
+        fpm1 = xmlrpclib.Binary('FP1faked_fpm1')
+        fpm2 = xmlrpclib.Binary('FP1faked_fpm2')
+        result = server.put_student_fingerprints(
+            self.student.student_id, {'1': fpm1, '3': fpm2})
+        self.assertEqual(result, True)
+        stored_file1 = getUtility(IExtFileStore).getFileByContext(
+            self.student, attr="finger1.fpm")
+        stored_file2 = getUtility(IExtFileStore).getFileByContext(
+            self.student, attr="finger3.fpm")
+        self.assertEqual(stored_file1.read(), 'FP1faked_fpm1')
+        self.assertEqual(stored_file2.read(), 'FP1faked_fpm2')
+
+    def test_get_student_fingerprints_no_stud(self):
+        # invalid student ids result in empty dict
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        result = server.get_student_fingerprints('invalid id')
+        self.assertEqual(result, {})
+
+    def test_get_student_fingerprints_no_files(self):
+        # we get student data, but no fingerprints if not stored before
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        result = server.get_student_fingerprints(self.student.student_id)
+        self.assertEqual(
+            result,
+            {'lastname': 'Tester',
+             'email': 'aa@aa.ng',
+             'firstname': 'Anna',
+             'fingerprints': {},
+             'img': None,
+             'img_name': None,
+             })
+
+    def test_get_student_fingerprints_passport(self):
+        # we get a photograph of the student if avail.
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.create_passport_img(self.student)
+        result = server.get_student_fingerprints(self.student.student_id)
+        self.assertTrue(
+            isinstance(result['img'], xmlrpclib.Binary))
+        self.assertEqual(result['img_name'], 'passport_K1000000.jpg')
+
+    def test_get_student_fingerprints_fpm(self):
+        # we get minutiae files if any are avail.
+        server = ServerProxy('http://mgr:mgrpw@localhost/app')
+        self.setup_student(self.student)
+        self.create_fpm_file(self.student, 'finger1')
+        result = server.get_student_fingerprints(self.student.student_id)
+        self.assertTrue('1' in result['fingerprints'].keys())
+        self.assertTrue(
+            isinstance(result['fingerprints']['1'], xmlrpclib.Binary))
+
+    def test_get_student_fingerprints_block_unauthorized(self):
+        # requests from unauthorized users are blocked
+        # no username nor password
+        server = ServerProxy('http://localhost/app')
+        self.setup_student(self.student)
+        stud_id = self.student.student_id
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, stud_id)
+        # wrong password
+        server = ServerProxy('http://mgr:WRONGPW@localhost/app')
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, stud_id)
+        # wrong username
+        server = ServerProxy('http://WRONGUSER:mgrpw@localhost/app')
+        self.assertRaises(
+            xmlrpclib.ProtocolError, server.get_student_id, stud_id)
+        return
+
+    def test_get_bursary_data(self):
+        tz = getUtility(IKofaUtils).tzinfo
+        payment1 = StudentOnlinePayment()
+        payment1.p_id = 'my-id1'
+        payment1.p_session = 2012
+        payment1.amount_auth = 12.12
+        payment1.p_state = u'paid'
+        payment1.p_category = u'schoolfee'
+        self.student['payments']['my-payment1'] = payment1
+        payment2 = StudentOnlinePayment()
+        payment2.p_id = 'my-id2'
+        payment2.p_session = 2012
+        payment2.amount_auth = 12.12
+        payment2.p_state = u'paid'
+        payment2.p_category = u'clearance'
+        self.student['payments']['my-payment2'] = payment2
+        server = ServerProxy('http://mgr:mgrpw@localhost/app', allow_none=True)
+        result = server.get_bursary_data(None,None,None,None,'dep1')
+        self.assertEqual(
+            result,[
+            {'entry_mode': None, 'reg_number': '123', 'display_item': None,
+                'firstname': 'Anna', 'payment_date': 'None#',
+                'middlename': None, 'student_id': 'K1000000', 'p_id':
+                'my-id1', 'certcode': 'CERT1', 'entry_session': 2004,
+                'creation_date': str('%s#' % to_timezone(payment1.creation_date, tz)),
+                'state': 'created', 'current_session': 2004,
+                'faccode': 'fac1', 'lastname': 'Tester',
+                'p_category': 'schoolfee', 'amount_auth': 12.12,
+                'p_state': 'paid', 'p_session': 2012, 'matric_number': '234',
+                'depcode': 'dep1'},
+            {'entry_mode': None, 'reg_number': '123', 'display_item': None,
+                'firstname': 'Anna', 'payment_date': 'None#',
+                'middlename': None, 'student_id': 'K1000000',
+                'p_id': 'my-id2', 'certcode': 'CERT1', 'entry_session': 2004,
+                'creation_date': str('%s#' % to_timezone(payment2.creation_date, tz)),
+                'state': 'created', 'current_session': 2004, 'faccode': 'fac1',
+                'lastname': 'Tester', 'p_category': 'clearance',
+                'amount_auth': 12.12, 'p_state': 'paid', 'p_session': 2012,
+                'matric_number': '234', 'depcode': 'dep1'}
+            ]
+            )
+
+        REQUEST_XML = """\
+<?xml version="1.0"?>
+<methodCall>
+<methodName>get_bursary_data</methodName>
+<params>
+<param>
+<value><string></string></value>
+<value><string></string></value>
+<value><string></string></value>
+<value><string></string></value>
+<value><string>dep1</string></value>
+</param>
+</params>
+</methodCall>"""
+        RESPONSE_XML = """\
+<?xml version='1.0'?>
+<methodResponse>
+<params>
+<param>
+<value><array><data>
+<value><struct>
+<member>
+<name>entry_mode</name>
+<value><nil/></value></member>
+<member>
+<name>reg_number</name>
+<value><string>123</string></value>
+</member>
+<member>
+<name>display_item</name>
+<value><nil/></value></member>
+<member>
+<name>firstname</name>
+<value><string>Anna</string></value>
+</member>
+<member>
+<name>payment_date</name>
+<value><string>None#</string></value>
+</member>
+<member>
+<name>middlename</name>
+<value><nil/></value></member>
+<member>
+<name>student_id</name>
+<value><string>K1000000</string></value>
+</member>
+<member>
+<name>p_id</name>
+<value><string>my-id1</string></value>
+</member>
+<member>
+<name>certcode</name>
+<value><string>CERT1</string></value>
+</member>
+<member>
+<name>creation_date</name>
+<value><string>%s</string></value>
+</member>
+<member>
+<name>matric_number</name>
+<value><string>234</string></value>
+</member>
+<member>
+<name>state</name>
+<value><string>created</string></value>
+</member>
+<member>
+<name>lastname</name>
+<value><string>Tester</string></value>
+</member>
+<member>
+<name>current_session</name>
+<value><int>2004</int></value>
+</member>
+<member>
+<name>faccode</name>
+<value><string>fac1</string></value>
+</member>
+<member>
+<name>entry_session</name>
+<value><int>2004</int></value>
+</member>
+<member>
+<name>p_category</name>
+<value><string>schoolfee</string></value>
+</member>
+<member>
+<name>amount_auth</name>
+<value><double>12.12</double></value>
+</member>
+<member>
+<name>p_session</name>
+<value><int>2012</int></value>
+</member>
+<member>
+<name>p_state</name>
+<value><string>paid</string></value>
+</member>
+<member>
+<name>depcode</name>
+<value><string>dep1</string></value>
+</member>
+</struct></value>
+<value><struct>
+<member>
+<name>entry_mode</name>
+<value><nil/></value></member>
+<member>
+<name>reg_number</name>
+<value><string>123</string></value>
+</member>
+<member>
+<name>display_item</name>
+<value><nil/></value></member>
+<member>
+<name>firstname</name>
+<value><string>Anna</string></value>
+</member>
+<member>
+<name>payment_date</name>
+<value><string>None#</string></value>
+</member>
+<member>
+<name>middlename</name>
+<value><nil/></value></member>
+<member>
+<name>student_id</name>
+<value><string>K1000000</string></value>
+</member>
+<member>
+<name>p_id</name>
+<value><string>my-id2</string></value>
+</member>
+<member>
+<name>certcode</name>
+<value><string>CERT1</string></value>
+</member>
+<member>
+<name>creation_date</name>
+<value><string>%s</string></value>
+</member>
+<member>
+<name>matric_number</name>
+<value><string>234</string></value>
+</member>
+<member>
+<name>state</name>
+<value><string>created</string></value>
+</member>
+<member>
+<name>lastname</name>
+<value><string>Tester</string></value>
+</member>
+<member>
+<name>current_session</name>
+<value><int>2004</int></value>
+</member>
+<member>
+<name>faccode</name>
+<value><string>fac1</string></value>
+</member>
+<member>
+<name>entry_session</name>
+<value><int>2004</int></value>
+</member>
+<member>
+<name>p_category</name>
+<value><string>clearance</string></value>
+</member>
+<member>
+<name>amount_auth</name>
+<value><double>12.12</double></value>
+</member>
+<member>
+<name>p_session</name>
+<value><int>2012</int></value>
+</member>
+<member>
+<name>p_state</name>
+<value><string>paid</string></value>
+</member>
+<member>
+<name>depcode</name>
+<value><string>dep1</string></value>
+</member>
+</struct></value>
+</data></array></value>
+</param>
+</params>
+</methodResponse>
+"""
+        xmlout = self.XMLRPC_post(REQUEST_XML)
+        response = RESPONSE_XML % (str('%s#' % to_timezone(payment1.creation_date, tz)),
+                                   str('%s#' % to_timezone(payment2.creation_date, tz))
+                                   )
+        self.assertEqual(xmlout, response)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/utils.py	(revision 16045)
@@ -0,0 +1,1229 @@
+## $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
+##
+"""General helper functions and utilities for the students section.
+"""
+import grok
+import textwrap
+from cgi import escape
+from time import time
+from cStringIO import StringIO
+from reportlab.lib import colors
+from reportlab.lib.units import cm
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph, Image, Table, Spacer
+from reportlab.platypus.doctemplate import LayoutError
+from reportlab.platypus.flowables import PageBreak
+from zope.event import notify
+from zope.schema.interfaces import ConstraintNotSatisfied
+from zope.component import getUtility, createObject, queryUtility
+from zope.catalog.interfaces import ICatalog
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from waeup.kofa.interfaces import (
+    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED, GRADUATED,
+    academic_sessions_vocab, IFileStoreNameChooser)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.workflow import ADMITTED
+from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
+from waeup.kofa.browser.pdf import (
+    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, 
+    get_signature_tables, get_qrcode)
+from waeup.kofa.browser.interfaces import IPDFCreator
+from waeup.kofa.utils.helpers import to_timezone
+
+SLIP_STYLE = [
+    ('VALIGN',(0,0),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
+    ]
+
+CONTENT_STYLE = [
+    ('VALIGN',(0,0),(-1,-1),'TOP'),
+    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
+    #('BACKGROUND',(0,0),(-1,0),colors.black),
+    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+    ('BOX', (0,0), (-1,-1), 1, colors.black),
+    ]
+
+FONT_SIZE = 10
+FONT_COLOR = 'black'
+
+def trans(text, lang):
+    # shortcut
+    return translate(text, 'waeup.kofa', target_language=lang)
+
+def formatted_text(text, color=FONT_COLOR, lang='en'):
+    """Turn `text`, `color` and `size` into an HTML snippet.
+
+    The snippet is suitable for use with reportlab and generating PDFs.
+    Wraps the `text` into a ``<font>`` tag with passed attributes.
+
+    Also non-strings are converted. Raw strings are expected to be
+    utf-8 encoded (usually the case for widgets etc.).
+
+    Finally, a br tag is added if widgets contain div tags
+    which are not supported by reportlab.
+
+    The returned snippet is unicode type.
+    """
+    if not isinstance(text, unicode):
+        if isinstance(text, basestring):
+            text = text.decode('utf-8')
+        else:
+            text = unicode(text)
+    if text == 'None':
+        text = ''
+    # Very long matriculation numbers need to be wrapped
+    if text.find(' ') == -1 and len(text.split('/')) > 6:
+        text = '/'.join(text.split('/')[:5]) + \
+            '/ ' + '/'.join(text.split('/')[5:])
+    # Mainly for boolean values we need our customized
+    # localisation of the zope domain
+    text = translate(text, 'zope', target_language=lang)
+    text = text.replace('</div>', '<br /></div>')
+    tag1 = u'<font color="%s">' % (color)
+    return tag1 + u'%s</font>' % text
+
+def generate_student_id():
+    students = grok.getSite()['students']
+    new_id = students.unique_student_id
+    return new_id
+
+def set_up_widgets(view, ignore_request=False):
+    view.adapters = {}
+    view.widgets = setUpEditWidgets(
+        view.form_fields, view.prefix, view.context, view.request,
+        adapters=view.adapters, for_display=True,
+        ignore_request=ignore_request
+        )
+
+def render_student_data(studentview, context, omit_fields=(),
+                        lang='en', slipname=None, no_passport=False):
+    """Render student table for an existing frame.
+    """
+    width, height = A4
+    set_up_widgets(studentview, ignore_request=True)
+    data_left = []
+    data_middle = []
+    style = getSampleStyleSheet()
+    img = getUtility(IExtFileStore).getFileByContext(
+        studentview.context, attr='passport.jpg')
+    if img is None:
+        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
+        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
+    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
+    data_left.append([doc_img])
+    #data.append([Spacer(1, 12)])
+
+    f_label = trans(_('Name:'), lang)
+    f_label = Paragraph(f_label, ENTRY1_STYLE)
+    f_text = formatted_text(studentview.context.display_fullname)
+    f_text = Paragraph(f_text, ENTRY1_STYLE)
+    data_middle.append([f_label,f_text])
+
+    for widget in studentview.widgets:
+        if 'name' in widget.name:
+            continue
+        f_label = translate(
+            widget.label.strip(), 'waeup.kofa',
+            target_language=lang)
+        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
+        f_text = formatted_text(widget(), lang=lang)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        data_middle.append([f_label,f_text])
+
+    if getattr(studentview.context, 'certcode', None):
+        if not 'certificate' in omit_fields:
+            f_label = trans(_('Study Course:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            f_text = formatted_text(
+                studentview.context['studycourse'].certificate.longtitle)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        if not 'department' in omit_fields:
+            f_label = trans(_('Department:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            f_text = formatted_text(
+                studentview.context[
+                'studycourse'].certificate.__parent__.__parent__.longtitle,
+                )
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        if not 'faculty' in omit_fields:
+            f_label = trans(_('Faculty:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            f_text = formatted_text(
+                studentview.context[
+                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
+                )
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        if not 'current_mode' in omit_fields:
+            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
+            sm = studymodes_dict[studentview.context.current_mode]
+            f_label = trans(_('Study Mode:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            f_text = formatted_text(sm)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        if not 'entry_session' in omit_fields:
+            f_label = trans(_('Entry Session:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            entry_session = studentview.context.entry_session
+            try:
+                entry_session = academic_sessions_vocab.getTerm(
+                    entry_session).title
+            except LookupError:
+                entry_session = _('void')
+            f_text = formatted_text(entry_session)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        # Requested by Uniben, does not really make sense
+        if not 'current_level' in omit_fields:
+            f_label = trans(_('Current Level:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            current_level = studentview.context['studycourse'].current_level
+            studylevelsource = StudyLevelSource().factory
+            current_level = studylevelsource.getTitle(
+                studentview.context, current_level)
+            f_text = formatted_text(current_level)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+        if not 'date_of_birth' in omit_fields:
+            f_label = trans(_('Date of Birth:'), lang)
+            f_label = Paragraph(f_label, ENTRY1_STYLE)
+            date_of_birth = studentview.context.date_of_birth
+            tz = getUtility(IKofaUtils).tzinfo
+            date_of_birth = to_timezone(date_of_birth, tz)
+            if date_of_birth is not None:
+                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
+            f_text = formatted_text(date_of_birth)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_middle.append([f_label,f_text])
+
+    if no_passport:
+        table = Table(data_middle,style=SLIP_STYLE)
+        table.hAlign = 'LEFT'
+        return table
+
+    # append QR code to the right
+    if slipname:
+        url = studentview.url(context, slipname)
+        data_right = [[get_qrcode(url, width=70.0)]]
+        table_right = Table(data_right,style=SLIP_STYLE)
+    else:
+        table_right = None
+
+    table_left = Table(data_left,style=SLIP_STYLE)
+    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
+    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
+    return table
+
+def render_table_data(tableheader, tabledata, lang='en'):
+    """Render children table for an existing frame.
+    """
+    data = []
+    #data.append([Spacer(1, 12)])
+    line = []
+    style = getSampleStyleSheet()
+    for element in tableheader:
+        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
+        field = Paragraph(field, style["Normal"])
+        line.append(field)
+    data.append(line)
+    for ticket in tabledata:
+        line = []
+        for element in tableheader:
+              field = formatted_text(getattr(ticket,element[1],u' '))
+              field = Paragraph(field, style["Normal"])
+              line.append(field)
+        data.append(line)
+    table = Table(data,colWidths=[
+        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
+    return table
+
+def render_transcript_data(view, tableheader, levels_data, lang='en'):
+    """Render children table for an existing frame.
+    """
+    data = []
+    style = getSampleStyleSheet()
+    format_float = getUtility(IKofaUtils).format_float
+    for level in levels_data:
+        level_obj = level['level']
+        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
+        headerline = []
+        tabledata = []
+        if 'evel' in view.level_dict.get('ticket.level', str(level_obj.level)):
+            subheader = '%s %s, %s' % (
+                trans(_('Session'), lang),
+                view.session_dict[level_obj.level_session],
+                view.level_dict.get('ticket.level', str(level_obj.level)))
+        else:
+            subheader = '%s %s, %s %s' % (
+                trans(_('Session'), lang),
+                view.session_dict[level_obj.level_session],
+                trans(_('Level'), lang),
+                view.level_dict.get(level_obj.level, str(level_obj.level)))
+        data.append(Paragraph(subheader, HEADING_STYLE))
+        for element in tableheader:
+            field = '<strong>%s</strong>' % formatted_text(element[0])
+            field = Paragraph(field, style["Normal"])
+            headerline.append(field)
+        tabledata.append(headerline)
+        for ticket in tickets:
+            ticketline = []
+            for element in tableheader:
+                  field = formatted_text(getattr(ticket,element[1],u' '))
+                  field = Paragraph(field, style["Normal"])
+                  ticketline.append(field)
+            tabledata.append(ticketline)
+        table = Table(tabledata,colWidths=[
+            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
+        data.append(table)
+        sgpa = format_float(level['sgpa'], 2)
+        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), sgpa)
+        #sgpa = '%s: %.2f' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
+        data.append(Paragraph(sgpa, style["Normal"]))
+        if getattr(level_obj, 'transcript_remark', None):
+            remark = '%s: %s' % (
+                trans('Transcript Remark', lang),
+                getattr(level_obj, 'transcript_remark'))
+            data.append(Paragraph(remark, style["Normal"]))
+    return data
+
+def docs_as_flowables(view, lang='en'):
+    """Create reportlab flowables out of scanned docs.
+    """
+    # XXX: fix circular import problem
+    from waeup.kofa.browser.fileviewlets import FileManager
+    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
+    style = getSampleStyleSheet()
+    data = []
+
+    # Collect viewlets
+    fm = FileManager(view.context, view.request, view)
+    fm.update()
+    if fm.viewlets:
+        sc_translation = trans(_('Scanned Documents'), lang)
+        data.append(Paragraph(sc_translation, HEADING_STYLE))
+        # Insert list of scanned documents
+        table_data = []
+        for viewlet in fm.viewlets:
+            if viewlet.file_exists:
+                # Show viewlet only if file exists
+                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
+                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
+                    view.context, attr=viewlet.download_name), 'name', None)
+                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
+                if img_path is None:
+                    pass
+                elif not img_path[-4:] in ('.jpg', '.JPG'):
+                    # reportlab requires jpg images, I think.
+                    f_text = Paragraph('%s (not displayable)' % (
+                        viewlet.title,), ENTRY1_STYLE)
+                else:
+                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
+                table_data.append([f_label, f_text])
+        if table_data:
+            # safety belt; empty tables lead to problems.
+            data.append(Table(table_data, style=SLIP_STYLE))
+    return data
+
+class StudentsUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(IStudentsUtils)
+
+    def getReturningData(self, student):
+        """ Define what happens after school fee payment
+        depending on the student's senate verdict.
+        In the base configuration current level is always increased
+        by 100 no matter which verdict has been assigned.
+        """
+        new_level = student['studycourse'].current_level + 100
+        new_session = student['studycourse'].current_session + 1
+        return new_session, new_level
+
+    def setReturningData(self, student):
+        """ Define what happens after school fee payment
+        depending on the student's senate verdict.
+        This method folllows the same algorithm as `getReturningData` but
+        it also sets the new values.
+        """
+        new_session, new_level = self.getReturningData(student)
+        try:
+            student['studycourse'].current_level = new_level
+        except ConstraintNotSatisfied:
+            # Do not change level if level exceeds the
+            # certificate's end_level.
+            pass
+        student['studycourse'].current_session = new_session
+        verdict = student['studycourse'].current_verdict
+        student['studycourse'].current_verdict = '0'
+        student['studycourse'].previous_verdict = verdict
+        return
+
+    def _getSessionConfiguration(self, session):
+        try:
+            return grok.getSite()['configuration'][str(session)]
+        except KeyError:
+            return None
+
+    def _isPaymentDisabled(self, p_session, category, student):
+        academic_session = self._getSessionConfiguration(p_session)
+        if category == 'schoolfee' and \
+            'sf_all' in academic_session.payment_disabled:
+            return True
+        return False
+
+    def samePaymentMade(self, student, category, p_item, p_session):
+        for key in student['payments'].keys():
+            ticket = student['payments'][key]
+            if ticket.p_state == 'paid' and\
+               ticket.p_category == category and \
+               ticket.p_item == p_item and \
+               ticket.p_session == p_session:
+                  return True
+        return False
+
+    def setPaymentDetails(self, category, student,
+            previous_session, previous_level, combi=[]):
+        """Create a payment ticket and set the payment data of a
+        student for the payment category specified.
+        """
+        p_item = u''
+        amount = 0.0
+        if previous_session:
+            if previous_session < student['studycourse'].entry_session:
+                return _('The previous session must not fall below '
+                         'your entry session.'), None
+            if category == 'schoolfee':
+                # School fee is always paid for the following session
+                if previous_session > student['studycourse'].current_session:
+                    return _('This is not a previous session.'), None
+            else:
+                if previous_session > student['studycourse'].current_session - 1:
+                    return _('This is not a previous session.'), None
+            p_session = previous_session
+            p_level = previous_level
+            p_current = False
+        else:
+            p_session = student['studycourse'].current_session
+            p_level = student['studycourse'].current_level
+            p_current = True
+        academic_session = self._getSessionConfiguration(p_session)
+        if academic_session == None:
+            return _(u'Session configuration object is not available.'), None
+        # Determine fee.
+        if category == 'schoolfee':
+            try:
+                certificate = student['studycourse'].certificate
+                p_item = certificate.code
+            except (AttributeError, TypeError):
+                return _('Study course data are incomplete.'), None
+            if previous_session:
+                # Students can pay for previous sessions in all
+                # workflow states.  Fresh students are excluded by the
+                # update method of the PreviousPaymentAddFormPage.
+                if previous_level == 100:
+                    amount = getattr(certificate, 'school_fee_1', 0.0)
+                else:
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
+            else:
+                if student.state == CLEARED:
+                    amount = getattr(certificate, 'school_fee_1', 0.0)
+                elif student.state == RETURNING:
+                    # In case of returning school fee payment the
+                    # payment session and level contain the values of
+                    # the session the student has paid for. Payment
+                    # session is always next session.
+                    p_session, p_level = self.getReturningData(student)
+                    academic_session = self._getSessionConfiguration(p_session)
+                    if academic_session == None:
+                        return _(
+                            u'Session configuration object is not available.'
+                            ), None
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
+                elif student.is_postgrad and student.state == PAID:
+                    # Returning postgraduate students also pay for the
+                    # next session but their level always remains the
+                    # same.
+                    p_session += 1
+                    academic_session = self._getSessionConfiguration(p_session)
+                    if academic_session == None:
+                        return _(
+                            u'Session configuration object is not available.'
+                            ), None
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
+        elif category == 'clearance':
+            try:
+                p_item = student['studycourse'].certificate.code
+            except (AttributeError, TypeError):
+                return _('Study course data are incomplete.'), None
+            amount = academic_session.clearance_fee
+        elif category == 'bed_allocation':
+            p_item = self.getAccommodationDetails(student)['bt']
+            amount = academic_session.booking_fee
+        elif category == 'hostel_maintenance':
+            amount = 0.0
+            bedticket = student['accommodation'].get(
+                str(student.current_session), None)
+            if bedticket is not None and bedticket.bed is not None:
+                p_item = bedticket.bed_coordinates
+                if bedticket.bed.__parent__.maint_fee > 0:
+                    amount = bedticket.bed.__parent__.maint_fee
+                else:
+                    # fallback
+                    amount = academic_session.maint_fee
+            else:
+                return _(u'No bed allocated.'), None
+        elif category == 'combi' and combi:
+            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
+            for cat in combi:
+                fee_name = cat + '_fee'
+                cat_amount = getattr(academic_session, fee_name, 0.0)
+                if not cat_amount:
+                    return _('%s undefined.' % categories[cat]), None
+                amount += cat_amount
+                p_item += u'%s + ' % categories[cat]
+            p_item = p_item.strip(' + ')
+        else:
+            fee_name = category + '_fee'
+            amount = getattr(academic_session, fee_name, 0.0)
+        if amount in (0.0, None):
+            return _('Amount could not be determined.'), None
+        if self.samePaymentMade(student, category, p_item, p_session):
+            return _('This type of payment has already been made.'), None
+        if self._isPaymentDisabled(p_session, category, student):
+            return _('This category of payments has been disabled.'), None
+        payment = createObject(u'waeup.StudentOnlinePayment')
+        timestamp = ("%d" % int(time()*10000))[1:]
+        payment.p_id = "p%s" % timestamp
+        payment.p_category = category
+        payment.p_item = p_item
+        payment.p_session = p_session
+        payment.p_level = p_level
+        payment.p_current = p_current
+        payment.amount_auth = amount
+        payment.p_combi = combi
+        return None, payment
+
+    def setBalanceDetails(self, category, student,
+            balance_session, balance_level, balance_amount):
+        """Create a balance payment ticket and set the payment data
+        as selected by the student.
+        """
+        p_item = u'Balance'
+        p_session = balance_session
+        p_level = balance_level
+        p_current = False
+        amount = balance_amount
+        academic_session = self._getSessionConfiguration(p_session)
+        if academic_session == None:
+            return _(u'Session configuration object is not available.'), None
+        if amount in (0.0, None) or amount < 0:
+            return _('Amount must be greater than 0.'), None
+        payment = createObject(u'waeup.StudentOnlinePayment')
+        timestamp = ("%d" % int(time()*10000))[1:]
+        payment.p_id = "p%s" % timestamp
+        payment.p_category = category
+        payment.p_item = p_item
+        payment.p_session = p_session
+        payment.p_level = p_level
+        payment.p_current = p_current
+        payment.amount_auth = amount
+        return None, payment
+
+    def increaseMatricInteger(self, student):
+        """Increase counter for matric numbers.
+        This counter can be a centrally stored attribute or an attribute of 
+        faculties, departments or certificates. In the base package the counter
+        is as an attribute of the site configuration container.
+        """
+        grok.getSite()['configuration'].next_matric_integer += 1
+        return
+
+    def constructMatricNumber(self, student):
+        """Fetch the matric number counter which fits the student and
+        construct the new matric number of the student.
+        In the base package the counter is returned which is as an attribute
+        of the site configuration container.
+        """
+        next_integer = grok.getSite()['configuration'].next_matric_integer
+        if next_integer == 0:
+            return _('Matriculation number cannot be set.'), None
+        return None, unicode(next_integer)
+
+    def setMatricNumber(self, student):
+        """Set matriculation number of student. If the student's matric number
+        is unset a new matric number is
+        constructed according to the matriculation number construction rules
+        defined in the `constructMatricNumber` method. The new matric number is
+        set, the students catalog updated. The corresponding matric number
+        counter is increased by one.
+
+        This method is tested but not used in the base package. It can
+        be used in custom packages by adding respective views
+        and by customizing `increaseMatricInteger` and `constructMatricNumber`
+        according to the university's matriculation number construction rules.
+
+        The method can be disabled by setting the counter to zero.
+        """
+        if student.matric_number is not None:
+            return _('Matriculation number already set.'), None
+        if student.certcode is None:
+            return _('No certificate assigned.'), None
+        error, matric_number = self.constructMatricNumber(student)
+        if error:
+            return error, None
+        try:
+            student.matric_number = matric_number
+        except MatNumNotInSource:
+            return _('Matriculation number %s exists.' % matric_number), None
+        notify(grok.ObjectModifiedEvent(student))
+        self.increaseMatricInteger(student)
+        return None, matric_number
+
+    def getAccommodationDetails(self, student):
+        """Determine the accommodation data of a student.
+        """
+        d = {}
+        d['error'] = u''
+        hostels = grok.getSite()['hostels']
+        d['booking_session'] = hostels.accommodation_session
+        d['allowed_states'] = hostels.accommodation_states
+        d['startdate'] = hostels.startdate
+        d['enddate'] = hostels.enddate
+        d['expired'] = hostels.expired
+        # Determine bed type
+        studycourse = student['studycourse']
+        certificate = getattr(studycourse,'certificate',None)
+        entry_session = studycourse.entry_session
+        current_level = studycourse.current_level
+        if None in (entry_session, current_level, certificate):
+            return d
+        end_level = certificate.end_level
+        if current_level == 10:
+            bt = 'pr'
+        elif entry_session == grok.getSite()['hostels'].accommodation_session:
+            bt = 'fr'
+        elif current_level >= end_level:
+            bt = 'fi'
+        else:
+            bt = 're'
+        if student.sex == 'f':
+            sex = 'female'
+        else:
+            sex = 'male'
+        special_handling = 'regular'
+        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
+        return d
+
+    def checkAccommodationRequirements(self, student, acc_details):
+        if acc_details.get('expired', False):
+            startdate = acc_details.get('startdate')
+            enddate = acc_details.get('enddate')
+            if startdate and enddate:
+                tz = getUtility(IKofaUtils).tzinfo
+                startdate = to_timezone(
+                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
+                enddate = to_timezone(
+                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
+                return _("Outside booking period: ${a} - ${b}",
+                         mapping = {'a': startdate, 'b': enddate})
+            else:
+                return _("Outside booking period.")
+        if not acc_details.get('bt'):
+            return _("Your data are incomplete.")
+        if not student.state in acc_details['allowed_states']:
+            return _("You are in the wrong registration state.")
+        if student['studycourse'].current_session != acc_details[
+            'booking_session']:
+            return _('Your current session does not '
+                     'match accommodation session.')
+        bsession = str(acc_details['booking_session'])
+        if bsession in student['accommodation'].keys() \
+            and not 'booking expired' in \
+            student['accommodation'][bsession].bed_coordinates:
+            return _('You already booked a bed space in '
+                     'current accommodation session.')
+        return
+
+    def selectBed(self, available_beds):
+        """Select a bed from a filtered list of available beds.
+        In the base configuration beds are sorted by the sort id
+        of the hostel and the bed number. The first bed found in
+        this sorted list is taken.
+        """
+        sorted_beds = sorted(available_beds,
+                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
+        return sorted_beds[0]
+
+    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
+        return ((1, 'Fail'),
+               (1.5, 'Pass'),
+               (2.4, '3rd Class'),
+               (3.5, '2nd Class Lower'),
+               (4.5, '2nd Class Upper'),
+               (5, '1st Class'))
+
+    def getClassFromCGPA(self, gpa, student):
+        """Determine the class of degree. In some custom packages
+        this class depends on e.g. the entry session of the student. In the
+        base package, it does not.
+        """
+        if gpa < self.GPABoundaries()[0][0]:
+            return 0, self.GPABoundaries()[0][1]
+        if gpa < self.GPABoundaries()[1][0]:
+            return 1, self.GPABoundaries()[1][1]
+        if gpa < self.GPABoundaries()[2][0]:
+            return 2, self.GPABoundaries()[2][1]
+        if gpa < self.GPABoundaries()[3][0]:
+            return 3, self.GPABoundaries()[3][1]
+        if gpa < self.GPABoundaries()[4][0]:
+            return 4, self.GPABoundaries()[4][1]
+        if gpa <= self.GPABoundaries()[5][0]:
+            return 5, self.GPABoundaries()[5][1]
+        return
+
+    def getDegreeClassNumber(self, level_obj):
+        """Get degree class number (used for SessionResultsPresentation
+        reports).
+        """
+        if level_obj.gpa_params[1] == 0:
+            # No credits weighted
+            return 6
+        return self.getClassFromCGPA(
+            level_obj.cumulative_params[0], level_obj.student)[0]
+
+    def _saveTranscriptPDF(self, student, transcript):
+        """Create a transcript PDF file and store it in student folder.
+        """
+        file_store = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(student).chooseName(
+            attr="final_transcript.pdf")
+        file_store.createFile(file_id, StringIO(transcript))
+        return
+
+    def warnCreditsOOR(self, studylevel, course=None):
+        """Return message if credits are out of range. In the base
+        package only maximum credits is set.
+        """
+        if course and studylevel.total_credits + course.credits > 50:
+            return _('Maximum credits exceeded.')
+        elif studylevel.total_credits > 50:
+            return _('Maximum credits exceeded.')
+        return
+
+    def warnCourseAlreadyPassed(self, studylevel, course):
+        """Return message if course has already been passed at
+        previous levels.
+        """
+        for slevel in studylevel.__parent__.values():
+            for cticket in slevel.values():
+                if cticket.code == course.code \
+                    and cticket.total_score >= cticket.passmark:
+                    return _('Course has already been passed at previous level.')
+        return False
+
+    def getBedCoordinates(self, bedticket):
+        """Return descriptive bed coordinates.
+        This method can be used to customize the `display_coordinates`
+        property method in order to  display a
+        customary description of the bed space.
+        """
+        return bedticket.bed_coordinates
+
+    def clearance_disabled_message(self, student):
+        """Render message if clearance is disabled.
+        """
+        try:
+            session_config = grok.getSite()[
+                'configuration'][str(student.current_session)]
+        except KeyError:
+            return _('Session configuration object is not available.')
+        if not session_config.clearance_enabled:
+            return _('Clearance is disabled for this session.')
+        return None
+
+    def getPDFCreator(self, context):
+        """Get a pdf creator suitable for `context`.
+        The default implementation always returns the default creator.
+        """
+        return getUtility(IPDFCreator)
+
+    def renderPDF(self, view, filename='slip.pdf', student=None,
+                  studentview=None,
+                  tableheader=[], tabledata=[],
+                  note=None, signatures=None, sigs_in_footer=(),
+                  show_scans=True, topMargin=1.5,
+                  omit_fields=()):
+        """Render pdf slips for various pages (also some pages
+        in the applicants module).
+        """
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        # XXX: tell what the different parameters mean
+        style = getSampleStyleSheet()
+        creator = self.getPDFCreator(student)
+        data = []
+        doc_title = view.label
+        author = '%s (%s)' % (view.request.principal.title,
+                              view.request.principal.id)
+        footer_text = view.label.split('\n')
+        if len(footer_text) > 1:
+            # We can add a department in first line, second line is used
+            footer_text = footer_text[1]
+        else:
+            # Only the first line is used for the footer
+            footer_text = footer_text[0]
+        if getattr(student, 'student_id', None) is not None:
+            footer_text = "%s - %s - " % (student.student_id, footer_text)
+
+        # Insert student data table
+        if student is not None:
+            if view.form_fields:
+                bd_translation = trans(_('Base Data'), portal_language)
+                data.append(Paragraph(bd_translation, HEADING_STYLE))
+            data.append(render_student_data(
+                studentview, view.context, omit_fields, lang=portal_language,
+                slipname=filename))
+
+        # Insert widgets
+        if view.form_fields:
+            data.append(Paragraph(view.title, HEADING_STYLE))
+            separators = getattr(self, 'SEPARATORS_DICT', {})
+            table = creator.getWidgetsTable(
+                view.form_fields, view.context, None, lang=portal_language,
+                separators=separators)
+            data.append(table)
+
+        # Insert scanned docs
+        if show_scans:
+            data.extend(docs_as_flowables(view, portal_language))
+
+        # Insert history
+        if filename == 'clearance_slip.pdf':
+            hist_translation = trans(_('Workflow History'), portal_language)
+            data.append(Paragraph(hist_translation, HEADING_STYLE))
+            data.extend(creator.fromStringList(student.history.messages))
+
+        # Insert content tables (optionally on second page)
+        if hasattr(view, 'tabletitle'):
+            for i in range(len(view.tabletitle)):
+                if tabledata[i] and tableheader[i]:
+                    tabletitle = view.tabletitle[i]
+                    if tabletitle.startswith('_PB_'):
+                        data.append(PageBreak())
+                        tabletitle = view.tabletitle[i].strip('_PB_')
+                    #data.append(Spacer(1, 20))
+                    data.append(Paragraph(tabletitle, HEADING_STYLE))
+                    data.append(Spacer(1, 8))
+                    contenttable = render_table_data(tableheader[i],tabledata[i])
+                    data.append(contenttable)
+
+        # Insert signatures
+        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
+        # do not have a test for the following lines.
+        if signatures and not sigs_in_footer:
+            data.append(Spacer(1, 20))
+            # Render one signature table per signature to
+            # get date and signature in line.
+            for signature in signatures:
+                signaturetables = get_signature_tables(signature)
+                data.append(signaturetables[0])
+
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        try:
+            pdf_stream = creator.create_pdf(
+                data, None, doc_title, author=author, footer=footer_text,
+                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
+        except IOError:
+            view.flash('Error in image file.')
+            return view.redirect(view.url(view.context))
+        except LayoutError, err:
+            view.flash(
+                'PDF file could not be created. Reportlab error message: %s'
+                % escape(err.message),
+                type="danger")
+            return view.redirect(view.url(view.context))
+        return pdf_stream
+
+    def _admissionText(self, student, portal_language):
+        inst_name = grok.getSite()['configuration'].name
+        text = trans(_(
+            'This is to inform you that you have been provisionally'
+            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
+            portal_language)
+        return text
+
+    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
+                                 pre_text=None, post_text=None,
+                                 topMargin = 1.5,
+                                 letterhead_path=None):
+        """Render pdf admission letter.
+        """
+        if student is None:
+            return
+        style = getSampleStyleSheet()
+        if letterhead_path:
+            creator = getUtility(IPDFCreator, name='letter')
+        else:
+            creator = self.getPDFCreator(student)
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        data = []
+        doc_title = view.label
+        author = '%s (%s)' % (view.request.principal.title,
+                              view.request.principal.id)
+        footer_text = view.label.split('\n')
+        if len(footer_text) > 1:
+            # We can add a department in first line
+            footer_text = footer_text[1]
+        else:
+            # Only the first line is used for the footer
+            footer_text = footer_text[0]
+        if getattr(student, 'student_id', None) is not None:
+            footer_text = "%s - %s - " % (student.student_id, footer_text)
+
+        # Text before student data
+        if pre_text is None:
+            html = format_html(self._admissionText(student, portal_language))
+        else:
+            html = format_html(pre_text)
+        if html:
+            data.append(Paragraph(html, NOTE_STYLE))
+            data.append(Spacer(1, 20))
+
+        # Student data
+        data.append(render_student_data(view, student,
+                    omit_fields, lang=portal_language,
+                    slipname='admission_slip.pdf'))
+
+        # Text after student data
+        data.append(Spacer(1, 20))
+        if post_text is None:
+            datelist = student.history.messages[0].split()[0].split('-')
+            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
+            post_text = trans(_(
+                'Your Kofa student record was created on ${a}.',
+                mapping = {'a': creation_date}),
+                portal_language)
+        #html = format_html(post_text)
+        #data.append(Paragraph(html, NOTE_STYLE))
+
+        # Create pdf stream
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        pdf_stream = creator.create_pdf(
+            data, None, doc_title, author=author, footer=footer_text,
+            note=post_text, topMargin=topMargin,
+            letterhead_path=letterhead_path)
+        return pdf_stream
+
+    def renderPDFTranscript(self, view, filename='transcript.pdf',
+                  student=None,
+                  studentview=None,
+                  note=None,
+                  signatures=(),
+                  sigs_in_footer=(),
+                  digital_sigs=(),
+                  show_scans=True, topMargin=1.5,
+                  omit_fields=(),
+                  tableheader=None,
+                  no_passport=False,
+                  save_file=False):
+        """Render pdf slip of a transcripts.
+        """
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        # XXX: tell what the different parameters mean
+        style = getSampleStyleSheet()
+        creator = self.getPDFCreator(student)
+        data = []
+        doc_title = view.label
+        author = '%s (%s)' % (view.request.principal.title,
+                              view.request.principal.id)
+        footer_text = view.label.split('\n')
+        if len(footer_text) > 2:
+            # We can add a department in first line
+            footer_text = footer_text[1]
+        else:
+            # Only the first line is used for the footer
+            footer_text = footer_text[0]
+        if getattr(student, 'student_id', None) is not None:
+            footer_text = "%s - %s - " % (student.student_id, footer_text)
+
+        # Insert student data table
+        if student is not None:
+            #bd_translation = trans(_('Base Data'), portal_language)
+            #data.append(Paragraph(bd_translation, HEADING_STYLE))
+            data.append(render_student_data(
+                studentview, view.context,
+                omit_fields, lang=portal_language,
+                slipname=filename,
+                no_passport=no_passport))
+
+        transcript_data = view.context.getTranscriptData()
+        levels_data = transcript_data[0]
+
+        contextdata = []
+        f_label = trans(_('Course of Study:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = formatted_text(view.context.certificate.longtitle)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        f_label = trans(_('Faculty:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = formatted_text(
+            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        f_label = trans(_('Department:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = formatted_text(
+            view.context.certificate.__parent__.__parent__.longtitle)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        f_label = trans(_('Entry Session:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = formatted_text(
+            view.session_dict.get(view.context.entry_session))
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        f_label = trans(_('Entry Mode:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        f_text = formatted_text(view.studymode_dict.get(
+            view.context.entry_mode))
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        f_label = trans(_('Cumulative GPA:'), portal_language)
+        f_label = Paragraph(f_label, ENTRY1_STYLE)
+        format_float = getUtility(IKofaUtils).format_float
+        cgpa = format_float(transcript_data[1], 3)
+        if student.state == GRADUATED:
+            f_text = formatted_text('%s (%s)' % (
+                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
+        else:
+            f_text = formatted_text('%s' % cgpa)
+        f_text = Paragraph(f_text, ENTRY1_STYLE)
+        contextdata.append([f_label,f_text])
+
+        contexttable = Table(contextdata,style=SLIP_STYLE)
+        data.append(contexttable)
+
+        transcripttables = render_transcript_data(
+            view, tableheader, levels_data, lang=portal_language)
+        data.extend(transcripttables)
+
+        # Insert signatures
+        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
+        # do not have a test for the following lines.
+        if signatures and not sigs_in_footer:
+            data.append(Spacer(1, 20))
+            # Render one signature table per signature to
+            # get date and signature in line.
+            for signature in signatures:
+                signaturetables = get_signature_tables(signature)
+                data.append(signaturetables[0])
+
+        # Insert digital signatures
+        if digital_sigs:
+            data.append(Spacer(1, 20))
+            sigs = digital_sigs.split('\n')
+            for sig in sigs:
+                data.append(Paragraph(sig, NOTE_STYLE))
+
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        try:
+            pdf_stream = creator.create_pdf(
+                data, None, doc_title, author=author, footer=footer_text,
+                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
+        except IOError:
+            view.flash(_('Error in image file.'))
+            return view.redirect(view.url(view.context))
+        if save_file:
+            self._saveTranscriptPDF(student, pdf_stream)
+            return
+        return pdf_stream
+
+    def renderPDFCourseticketsOverview(
+            self, view, name, session, data, lecturers, orientation,
+            title_length, note):
+        """Render pdf slip of course tickets for a lecturer.
+        """
+        filename = '%s_%s_%s_%s.pdf' % (
+            name, view.context.code, session, view.request.principal.id)
+        try:
+            session = academic_sessions_vocab.getTerm(session).title
+        except LookupError:
+            session = _('void')
+        creator = getUtility(IPDFCreator, name=orientation)
+        style = getSampleStyleSheet()
+        pdf_data = []
+        pdf_data += [Paragraph(
+            translate(_('<b>Lecturer(s): ${a}</b>',
+                      mapping = {'a':lecturers})), style["Normal"]),]
+        pdf_data += [Paragraph(
+            translate(_('<b>Credits: ${a}</b>',
+                      mapping = {'a':view.context.credits})), style["Normal"]),]
+        # Not used in base package.
+        if data[1]:
+            pdf_data += [Paragraph(
+                translate(_('<b>${a}</b>',
+                    mapping = {'a':data[1][0]})), style["Normal"]),]
+            pdf_data += [Paragraph(
+                translate(_('<b>${a}</b>',
+                    mapping = {'a':data[1][1]})), style["Normal"]),]
+            pdf_data += [Paragraph(
+                translate(_('<b>Total Students: ${a}</b>',
+                    mapping = {'a':data[1][2]})), style["Normal"]),]
+            pdf_data += [Paragraph(
+                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
+                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
+            pdf_data += [Paragraph(
+                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
+                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
+            grade_stats = []
+            for item in sorted(data[1][7].items()):
+                grade_stats.append(('%s=%s' % (item[0], item[1])))
+            grade_stats_string = ', '.join(grade_stats)
+            pdf_data += [Paragraph(
+                translate(_('<b>${a}</b>',
+                mapping = {'a':grade_stats_string})), style["Normal"]),]
+        pdf_data.append(Spacer(1, 20))
+        colWidths = [None] * len(data[0][0])
+        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
+                           repeatRows=1)]
+        # Process title if too long
+        title = " ".join(view.context.title.split())
+        ct = textwrap.fill(title, title_length)
+        ft = "" # title
+        #if len(textwrap.wrap(title, title_length)) > 1:
+        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
+        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
+            mapping = {'a':ct,
+                       'b':view.context.code,
+                       'd':session}))
+        if name == 'attendance':
+            doc_title += '\n' + translate(_('Attendance Sheet'))
+        if name == 'coursetickets':
+            doc_title += '\n' + translate(_('Course Tickets Overview'))
+        #footer_title = translate(_('${a} (${b}) - ${d}',
+        #    mapping = {'a':ft,
+        #               'b':view.context.code,
+        #               'd':session}))
+        footer_title = translate(_('${b} - ${d}',
+            mapping = {'b':view.context.code,
+                       'd':session}))
+        author = '%s (%s)' % (view.request.principal.title,
+                              view.request.principal.id)
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        view.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        pdf_stream = creator.create_pdf(
+            pdf_data, None, doc_title, author, footer_title + ' -', note
+            )
+        return pdf_stream
+
+    def updateCourseTickets(self, course):
+        """Udate course tickets if course attributes were changed.
+        """
+        current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if not current_academic_session:
+            return
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        coursetickets = cat.searchResults(
+            code=(course.code, course.code),
+            session=(current_academic_session,current_academic_session))
+        number = 0
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
+        for ticket in coursetickets:
+            if ticket.credits == course.credits:
+                continue
+            if ticket.student.current_session != current_academic_session:
+                continue
+            if ticket.student.state not in (PAID,):
+                continue
+            number += 1
+            ticket.student.__parent__.logger.info(
+                '%s - %s %s/%s credits updated (%s->%s)' % (
+                    ob_class, ticket.student.student_id,
+                    ticket.level, ticket.code, course.credits,
+                    ticket.credits))
+            ticket.credits = course.credits
+        return number
+
+    #: A dictionary which maps widget names to headlines. The headline
+    #: is rendered in forms and on pdf slips above the respective
+    #: display or input widget. There are no separating headlines
+    #: in the base package.
+    SEPARATORS_DICT = {}
+
+    #: A tuple containing names of file upload viewlets which are not shown
+    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
+    #: in the base package. This attribute makes only sense, if intermediate
+    #: custom packages are being used, like we do for all Nigerian portals.
+    SKIP_UPLOAD_VIEWLETS = ()
+
+    #: A tuple containing the names of registration states in which changing of
+    #: passport pictures is allowed.
+    PORTRAIT_CHANGE_STATES = (ADMITTED,)
+
+    #: A tuple containing all exporter names referring to students or
+    #: subobjects thereof.
+    STUDENT_EXPORTER_NAMES = (
+            'students',
+            'studentstudycourses',
+            'studentstudylevels',
+            'coursetickets',
+            'studentpayments',
+            'bedtickets',
+            'trimmed',
+            'outstandingcourses',
+            'unpaidpayments',
+            'sfpaymentsoverview',
+            'sessionpaymentsoverview',
+            'studylevelsoverview',
+            'combocard',
+            'bursary',
+            'accommodationpayments',
+            'transcriptdata',
+            'trimmedpayments',
+            )
+
+    #: A tuple containing all exporter names needed for backing
+    #: up student data
+    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
+            'studentstudylevels', 'coursetickets',
+            'studentpayments', 'bedtickets')
+
+    # Maximum size of upload files in kB
+    MAX_KB = 250
+
+    #: A prefix used when generating new student ids. Each student id will
+    #: start with this string. The default is 'K' for Kofa.
+    STUDENT_ID_PREFIX = u'K'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/viewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/viewlets.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/viewlets.py	(revision 16045)
@@ -0,0 +1,934 @@
+## $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
+##
+import grok
+from zope.component import getUtility
+from zope.i18n import translate
+from zope.interface import Interface
+from waeup.kofa.browser.layout import default_primary_nav_template
+from waeup.kofa.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import IExtFileStore, IKofaObject
+from waeup.kofa.students.browser import (
+    StudentsContainerPage, StudentsContainerManagePage,
+    StudentBaseDisplayFormPage, StudentClearanceDisplayFormPage,
+    StudentPersonalDisplayFormPage, StudyCourseDisplayFormPage,
+    StudyLevelDisplayFormPage, CourseTicketDisplayFormPage,
+    OnlinePaymentDisplayFormPage, BedTicketDisplayFormPage,
+    StudentClearanceEditFormPage, StudentPersonalEditFormPage,
+    PaymentsManageFormPage, StudyCourseTranscriptPage)
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentStudyCourse, IStudentStudyLevel,
+    ICourseTicket, IStudentOnlinePayment, IBedTicket,
+    IStudentPaymentsContainer, IStudentsUtils,
+    )
+from waeup.kofa.students.workflow import (
+    ADMITTED, PAID, REQUESTED, CLEARED, REGISTERED, VALIDATED, GRADUATED,
+    TRANSREQ, TRANSVAL, TRANSREL)
+
+
+grok.context(IKofaObject)  # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+
+class StudentManageSidebar(grok.ViewletManager):
+    grok.name('left_studentmanage')
+
+
+class StudentManageLink(grok.Viewlet):
+    """A link displayed in the student box which shows up for StudentNavigation
+    objects.
+
+    """
+    grok.baseclass()
+    grok.viewletmanager(StudentManageSidebar)
+    grok.context(IKofaObject)
+    grok.view(Interface)
+    grok.order(5)
+    grok.require('waeup.viewStudent')
+
+    link = 'index'
+    text = _(u'Base Data')
+
+    def render(self):
+        url = self.view.url(self.context.student, self.link)
+        # Here we know that the cookie has been set
+        lang = self.request.cookies.get('kofa.language')
+        text = translate(
+            self.text, 'waeup.kofa', target_language=lang)
+        if not self.link:
+            return ''
+        return u'<li><a href="%s">%s</a></li>' % (
+            url, text)
+
+
+class StudentManageApplicationLink(StudentManageLink):
+    grok.order(1)
+    link = 'application_slip'
+    text = _(u'Application Slip')
+
+    def render(self):
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr=self.link)
+        if slip:
+            lang = self.request.cookies.get('kofa.language')
+            text = translate(
+                self.text, 'waeup.kofa', target_language=lang)
+            url = self.view.url(self.context.student, self.link)
+            return u'<li><a href="%s">%s</a></li>' % (
+                url, text)
+        return ''
+
+class StudentManageBaseLink(StudentManageLink):
+    grok.order(2)
+    link = 'index'
+    text = _(u'Base Data')
+
+
+class StudentManageClearanceLink(StudentManageLink):
+    grok.order(3)
+    grok.name('studentmanageclearancelink')
+    link = 'view_clearance'
+    text = _(u'Clearance Data')
+
+
+class StudentManagePersonalLink(StudentManageLink):
+    grok.order(4)
+    grok.name('studentmanagepersonallink')
+    link = 'view_personal'
+    text = _(u'Personal Data')
+
+
+class StudentManageStudyCourseLink(StudentManageLink):
+    grok.order(5)
+    link = 'studycourse'
+    text = _(u'Study Course')
+
+
+class StudentManagePaymentsLink(StudentManageLink):
+    grok.order(6)
+    grok.require('waeup.viewStudent')
+    link = 'payments'
+    text = _(u'Payments')
+
+
+class StudentManageAccommodationLink(StudentManageLink):
+    grok.order(7)
+    grok.name('studentmanageaccommodationlink')
+    grok.require('waeup.viewStudent')
+    link = 'accommodation'
+    text = _(u'Accommodation')
+
+
+class StudentManageHistoryLink(StudentManageLink):
+    grok.order(8)
+    link = 'history'
+    text = _(u'History')
+
+
+class StudentManageTranscriptLink(StudentManageLink):
+    grok.order(9)
+    link = 'final_transcript'
+    text = _(u'Final Transcript')
+
+    def render(self):
+        slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr=self.link)
+        if slip:
+            lang = self.request.cookies.get('kofa.language')
+            text = translate(
+                self.text, 'waeup.kofa', target_language=lang)
+            url = self.view.url(self.context.student, self.link)
+            return u'<li><a href="%s">%s</a></li>' % (
+                url, text)
+        return ''
+
+
+class StudentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentsContainer)
+    grok.view(StudentsContainerPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage students section')
+
+
+class StudentsContainerAddActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(IStudentsContainer)
+    grok.view(StudentsContainerManagePage)
+    grok.require('waeup.manageStudent')
+    text = _('Add student')
+    target = 'addstudent'
+
+
+class ContactActionButton(ManageActionButton):
+    grok.order(5)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    icon = 'actionicon_mail.png'
+    text = _('Send email')
+    target = 'contactstudent'
+
+
+class StudentBaseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage_base'
+
+
+class StudentTrigTransActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.triggerTransition')
+    icon = 'actionicon_trigtrans.png'
+    text = _(u'Trigger transition')
+    target = 'trigtrans'
+
+
+class StudentLoginAsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.loginAsStudent')
+    icon = 'actionicon_mask.png'
+    text = _(u'Login as student')
+    target = 'loginasstep1'
+
+
+class AdmissionSlipActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download admission letter')
+    target = 'admission_slip.pdf'
+
+
+class StudentTransferButton(ManageActionButton):
+    grok.order(6)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Transfer student')
+    target = 'transfer'
+    icon = 'actionicon_redo.png'
+
+
+class StudentDeactivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Deactivate account')
+    target = 'deactivate'
+    icon = 'actionicon_traffic_lights_red.png'
+
+    @property
+    def target_url(self):
+        if self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+
+class StudentActivateActionButton(ManageActionButton):
+    grok.order(7)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Activate account')
+    target = 'activate'
+    icon = 'actionicon_traffic_lights_green.png'
+
+    @property
+    def target_url(self):
+        if not self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+
+class StudentClearanceManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage_clearance'
+
+
+class StudentClearActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.clearStudent')
+    text = _('Clear student')
+    target = 'clear'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        cdm = getUtility(
+            IStudentsUtils).clearance_disabled_message(self.context)
+        if cdm:
+            return ''
+        if self.context.state != REQUESTED:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentRejectClearanceActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.clearStudent')
+    text = _('Reject clearance')
+    target = 'reject_clearance'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        cdm = getUtility(
+            IStudentsUtils).clearance_disabled_message(self.context)
+        if cdm:
+            return ''
+        if self.context.state not in (REQUESTED, CLEARED):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class ClearanceSlipActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download clearance slip')
+    target = 'clearance_slip.pdf'
+
+
+class ClearanceViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceEditFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_view.png'
+    text = _('View')
+    target = 'view_clearance'
+
+
+class PersonalViewActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentPersonalEditFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_view.png'
+    text = _('View')
+    target = 'view_personal'
+
+
+class StudentPersonalManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentPersonalDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage_personal'
+
+
+class StudentPersonalEditActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudent)
+    grok.view(StudentPersonalDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Edit')
+    target = 'edit_personal'
+
+
+class StudyCourseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage'
+
+    @property
+    def target_url(self):
+        if self.context.is_current:
+            return self.view.url(self.view.context, self.target)
+        return False
+
+
+class RevertTransferActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    icon = 'actionicon_undo.png'
+    text = _('Reactivate this study course (revert previous transfer)')
+    target = 'revert_transfer'
+
+    @property
+    def target_url(self):
+        if self.context.is_previous:
+            return self.view.url(self.view.context.__parent__, self.target)
+        return False
+
+
+class StudyLevelManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage'
+
+    @property
+    def target_url(self):
+        is_current = self.context.__parent__.is_current
+        if not is_current:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentValidateCoursesActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.validateStudent')
+    text = _('Validate courses')
+    target = 'validate_courses'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state != REGISTERED:
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentRejectCoursesActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.validateStudent')
+    text = _('Reject courses')
+    target = 'reject_courses'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state not in (VALIDATED, REGISTERED):
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentUnregisterCoursesActionButton(ManageActionButton):
+    grok.order(5)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Unregister courses')
+    target = 'unregister_courses'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'You really want to unregister your course list?'")
+
+    @property
+    def target_url(self):
+        if not self.context.__parent__.is_current:
+            return ''
+        if self.context.student.state != REGISTERED:
+            return ''
+        if str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class CourseRegistrationSlipActionButton(ManageActionButton):
+    grok.order(6)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download course registration slip')
+    target = 'course_registration_slip.pdf'
+
+    @property
+    def target_url(self):
+        is_current = self.context.__parent__.is_current
+        if not is_current:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class CourseTicketManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(ICourseTicket)
+    grok.view(CourseTicketDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage'
+
+
+class PaymentReceiptActionButton(ManageActionButton):
+    grok.order(9)  # This button should always be the last one.
+    grok.context(IStudentOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download payment slip')
+    target = 'payment_slip.pdf'
+
+    @property
+    def target_url(self):
+        #if self.context.p_state != 'paid':
+        #    return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class ApprovePaymentActionButton(ManageActionButton):
+    grok.order(8)
+    grok.context(IStudentOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.managePortal')
+    icon = 'actionicon_accept.png'
+    text = _('Approve payment')
+    target = 'approve'
+
+    @property
+    def target_url(self):
+        if self.context.p_state in ('paid', 'waived', 'scholarship'):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class BedTicketSlipActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IBedTicket)
+    grok.view(BedTicketDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download bed allocation slip')
+    target = 'bed_allocation_slip.pdf'
+
+
+class RelocateStudentActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IBedTicket)
+    grok.view(BedTicketDisplayFormPage)
+    grok.require('waeup.manageHostels')
+    icon = 'actionicon_reload.png'
+    text = _('Relocate student')
+    target = 'relocate'
+
+
+class StudentBaseActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Edit')
+    target = 'edit_base'
+
+
+class StudentPasswordActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    icon = 'actionicon_key.png'
+    text = _('Change password')
+    target = 'change_password'
+
+
+class StudentPassportActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    icon = 'actionicon_portrait.png'
+    text = _('Change portrait')
+    target = 'change_portrait'
+
+    @property
+    def target_url(self):
+        PORTRAIT_CHANGE_STATES = getUtility(
+            IStudentsUtils).PORTRAIT_CHANGE_STATES
+        if self.context.state not in PORTRAIT_CHANGE_STATES:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentClearanceStartActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    icon = 'actionicon_start.gif'
+    text = _('Start clearance')
+    target = 'start_clearance'
+
+    @property
+    def target_url(self):
+        if self.context.state != ADMITTED:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudentClearanceEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Edit')
+    target = 'cedit'
+
+    @property
+    def target_url(self):
+        if self.context.clearance_locked:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StartSessionActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    icon = 'actionicon_start.gif'
+    text = _('Start new session')
+    target = 'start_session'
+
+    @property
+    def target_url(self):
+        if self.context.next_session_allowed and self.context.is_current:
+            return self.view.url(self.view.context, self.target)
+        return False
+
+
+class AddStudyLevelActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Add course list')
+    target = 'add'
+
+    @property
+    def target_url(self):
+        student = self.view.context.student
+        condition1 = student.state != PAID
+        condition2 = str(student['studycourse'].current_level) in \
+            self.view.context.keys()
+        condition3 = not self.context.is_current
+        if condition1 or condition2 or condition3:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudyLevelEditActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.editStudyLevel')
+    text = _('Edit course list')
+    target = 'edit'
+
+    @property
+    def target_url(self):
+        student = self.view.context.student
+        condition1 = student.state == PAID
+        condition2 = self.view.context.is_current_level
+        is_current = self.context.__parent__.is_current
+        if condition1 and condition2 and is_current:
+            return self.view.url(self.view.context, self.target)
+        return ''
+
+
+class AddPaymentActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(IStudentPaymentsContainer)
+    grok.view(PaymentsManageFormPage)
+    grok.require('waeup.payStudent')
+    text = _('Add current session payment ticket')
+    target = 'addop'
+
+
+class AddPreviousPaymentActionButton(AddActionButton):
+    grok.order(2)
+    grok.context(IStudentPaymentsContainer)
+    grok.view(PaymentsManageFormPage)
+    grok.require('waeup.payStudent')
+    grok.name('addpreviouspaymentactionbutton')
+    text = _('Add previous session payment ticket')
+    target = 'addpp'
+
+    @property
+    def target_url(self):
+        student = self.view.context.student
+        if student.before_payment or not self.target:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class AddBalancePaymentActionButton(AddActionButton):
+    grok.order(3)
+    grok.context(IStudentPaymentsContainer)
+    grok.view(PaymentsManageFormPage)
+    grok.require('waeup.manageStudent')
+    grok.name('addbalancepaymentactionbutton')
+    text = _('Add balance payment ticket')
+    target = 'addbp'
+
+    @property
+    def target_url(self):
+        if not self.target:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class StudyCourseTranscriptActionButton(ManageActionButton):
+    grok.order(2)
+    grok.name('transcript')
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.viewTranscript')
+    text = _('Transcript')
+    target = 'transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        final_slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr='final_transcript')
+        if self.context.student.transcript_enabled and not final_slip:
+            return self.view.url(self.view.context, self.target)
+        return False
+
+
+class RequestTranscriptActionButton(ManageActionButton):
+    grok.order(8)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Request transcript')
+    target = 'request_transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.state != GRADUATED:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class ValidateTranscriptActionButton(ManageActionButton):
+    grok.order(8)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseTranscriptPage)
+    grok.require('waeup.processTranscript')
+    text = _('Validate transcript')
+    target = 'validate_transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.student.state != TRANSREQ:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class ReleaseTranscriptActionButton(ManageActionButton):
+    grok.order(8)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseTranscriptPage)
+    grok.require('waeup.processTranscript')
+    text = _('Release transcript')
+    target = 'release_transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.student.state != TRANSVAL:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
+class TranscriptSlipActionButton(ManageActionButton):
+    grok.order(1)
+    grok.name('transcript_slip')
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseTranscriptPage)
+    grok.require('waeup.viewTranscript')
+    text = _('Academic Transcript')
+    target = 'transcript.pdf'
+    icon = 'actionicon_pdf.png'
+
+    @property
+    def target_url(self):
+        final_slip = getUtility(IExtFileStore).getFileByContext(
+            self.context.student, attr='final_transcript')
+        if self.context.student.transcript_enabled \
+            and not final_slip:
+            return self.view.url(self.view.context, self.target)
+        return False
+
+
+class SignTranscriptActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseTranscriptPage)
+    grok.require('waeup.signTranscript')
+    text = _('Sign transcript electronically')
+    target = 'sign_transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.student.state != TRANSVAL:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'Signing a transcript electronically cannot be revoked. "
+            "The electronic signature replaces your handwritten signature.\\n\\n"
+            "You really want to sign the transcript?'")
+
+
+class StudentsTab(PrimaryNavTab):
+    """Students tab in primary navigation.
+    """
+    grok.context(IKofaObject)
+    grok.order(4)
+    grok.require('waeup.viewStudentsContainer')
+    grok.name('studentstab')
+
+    pnav = 4
+    tab_title = _(u'Students')
+
+    @property
+    def link_target(self):
+        return self.view.application_url('students')
+
+
+class PrimaryStudentNavManager(grok.ViewletManager):
+    """Viewlet manager for the primary navigation tab.
+    """
+    grok.name('primary_nav_student')
+
+
+class PrimaryStudentNavTab(grok.Viewlet):
+    """Base for primary student nav tabs.
+    """
+    grok.baseclass()
+    grok.context(IKofaObject)
+    grok.viewletmanager(PrimaryStudentNavManager)
+    template = default_primary_nav_template
+    grok.order(1)
+    grok.require('waeup.Authenticated')
+    pnav = 0
+    tab_title = u'Some Text'
+
+    @property
+    def link_target(self):
+        return self.view.application_url()
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active'
+        return ''
+
+
+class MyStudentDataTab(PrimaryStudentNavTab):
+    """MyData dropdown tab in primary navigation.
+    """
+    grok.order(3)
+    grok.require('waeup.viewMyStudentDataTab')
+    grok.template('mydatadropdowntabs')
+    grok.name('mystudentdatatab')
+    pnav = 4
+    tab_title = _(u'My Data')
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        if view_pnav == self.pnav:
+            return 'active dropdown'
+        return 'dropdown'
+
+    @property
+    def targets(self):
+        student = grok.getSite()['students'][self.request.principal.id]
+        student_url = self.view.url(student)
+        app_slip = getUtility(IExtFileStore).getFileByContext(
+            student, 'application_slip')
+        targets = []
+        if student.getParentsPassword():
+            targets += [
+                {'url': student_url, 'title': 'Base Data'},
+                {'url': student_url + '/studycourse', 'title': _('Study Course')},
+                {'url': student_url + '/payments', 'title': _('Payments')},
+                ]
+            return targets
+        if app_slip:
+            targets = [{'url': student_url + '/application_slip',
+                        'title': _('Application Slip')}, ]
+        targets += [
+            {'url': student_url, 'title': 'Base Data'},
+            {'url': student_url + '/view_clearance',
+             'title': _('Clearance Data')},
+            {'url': student_url + '/view_personal',
+             'title': _('Personal Data')},
+            {'url': student_url + '/studycourse', 'title': _('Study Course')},
+            {'url': student_url + '/payments', 'title': _('Payments')},
+            {'url': student_url + '/accommodation',
+             'title': _('Accommodation Data')},
+            {'url': student_url + '/history', 'title': _('History')},
+            ]
+        return targets
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/vocabularies.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/vocabularies.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/vocabularies.py	(revision 16045)
@@ -0,0 +1,229 @@
+## $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
+##
+"""Vocabularies and sources for the students section.
+"""
+from zope.component import getUtility, queryUtility
+from zope.catalog.interfaces import ICatalog
+from zope.interface import implements, directlyProvides
+from zope.schema.interfaces import ISource, IContextSourceBinder
+from zope.schema.interfaces import ValidationError
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.kofa.interfaces import SimpleKofaVocabulary
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import get_sorted_preferred
+from waeup.kofa.utils.countries import COUNTRIES
+from waeup.kofa.university.vocabularies import course_levels
+from waeup.kofa.university.certificate import Certificate
+
+
+#: a tuple of tuples (<COUNTRY-NAME>, <ISO-CODE>) with Nigeria first.
+COUNTRIES = get_sorted_preferred(COUNTRIES, ['NG'])
+nats_vocab = SimpleKofaVocabulary(*COUNTRIES)
+
+def levels_from_range(start_level=None, end_level=None):
+    """Get a list of valid levels for given start/end level.
+
+    Start and end level must be numbers (or ``None``).
+
+    If no start/end level is given, return the default list of levels.
+
+    If only one of start_level or end_level is given, the result is
+    undefined.
+    """
+    if 999 in (start_level, end_level):
+        return [999]
+    levels = [10,] + [
+        level for level in range(100,1000,10) if level % 100 < 30]
+    if start_level is None and end_level is None:
+        return levels + [999]
+    if start_level == 10 == end_level:
+        return [10,]
+    start_level, end_level = (start_level or 10, end_level or 900)
+    levels = [x for x in levels
+              if x >= start_level and x <= (end_level + 120)]
+    return levels
+
+def study_levels(context):
+    if isinstance(context, Certificate):
+        certificate = context
+    else:
+        certificate = getattr(context, 'certificate', None)
+    start_level, end_level = (None, None)
+    if  certificate is not None:
+        start_level = int(certificate.start_level)
+        end_level = int(certificate.end_level)
+    return [0,] + levels_from_range(start_level, end_level)
+
+class StudyLevelSource(BasicContextualSourceFactory):
+    """The StudyLevelSource is based on and extends the
+    course_levels vocabulary defined in the university package.
+    Repeating study levels are denoted by increments of 10, the
+    first spillover level by the certificate's end level plus 100
+    and the second spillover level by the end level plus 110.
+    The level zero contains all courses without level
+    affiliation.
+    """
+    def getValues(self, context):
+        return study_levels(context)
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        if isinstance(context, Certificate):
+            # We use this source also for various reports.
+            # In such a case the context of the source will be the
+            # certificate itself.
+            certificate = context
+        else:
+            certificate = getattr(context, 'certificate', None)
+        if certificate is not None:
+            start_level = int(certificate.start_level)
+            end_level = int(certificate.end_level)
+        else:
+            # default level range
+            start_level = 10
+            end_level = 1000
+        if 999 in (start_level, end_level):
+            if value != 999:
+                return _('Error: wrong level id ${value}',
+                    mapping={'value': value})
+        if value == 999:
+            return course_levels.by_value[999].title
+        if value == 0:
+            return _('Level Zero')
+        if start_level == 10 and end_level == 10 and value != 10:
+            return _('Error: level id ${value} out of range',
+                mapping={'value': value})
+        if value < start_level or value > end_level + 120:
+            return _('Error: level id ${value} out of range',
+                mapping={'value': value})
+        # Special treatment for pre-studies level
+        if value == 10:
+            return course_levels.by_value[value].title
+        level,repeat = divmod(value, 100)
+        level = level * 100
+        repeat = repeat//10
+        title = course_levels.by_value[level].title
+        if level > end_level and repeat == 1:
+            title = course_levels.by_value[level - 100].title
+            return _('${title} 2nd spillover', mapping={'title': title})
+        if level > end_level and repeat == 2:
+            title = course_levels.by_value[level - 100].title
+            return _('${title} 3rd spillover', mapping={'title': title})
+        if level > end_level:
+            title = course_levels.by_value[level - 100].title
+            return  _('${title} 1st spillover', mapping={'title': title})
+        if repeat == 1:
+            return _('${title} on 1st probation', mapping={'title': title})
+        if repeat == 2:
+            return _('${title} on 2nd probation', mapping={'title': title})
+        return title
+
+class GenderSource(BasicSourceFactory):
+    """A gender source delivers basically a mapping
+       ``{'m': 'Male', 'f': 'Female'}``
+
+       Using a source, we make sure that the tokens (which are
+       stored/expected for instance from CSV files) are something one
+       can expect and not cryptic IntIDs.
+    """
+    def getValues(self):
+        return ['m', 'f']
+
+    def getToken(self, value):
+        return value[0].lower()
+
+    def getTitle(self, value):
+        if value == 'm':
+            return _('male')
+        if value == 'f':
+            return _('female')
+
+class RegNumNotInSource(ValidationError):
+    """Registration number exists already
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class MatNumNotInSource(ValidationError):
+    """Matriculation number exists already
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class RegNumberSource(object):
+    """A source that accepts any entry for a certain field if not used
+    already.
+
+    Using this kind of source means a way of setting an invariant.
+
+    We accept a value iff:
+    - the value cannot be found in catalog or
+    - the value can be found as part of some item but the bound item
+    is the context object itself.
+    """
+    implements(ISource)
+    cat_name = 'students_catalog'
+    field_name = 'reg_number'
+    validation_error = RegNumNotInSource
+    comp_field = 'student_id'
+    def __init__(self, context):
+        self.context = context
+        return
+
+    def __contains__(self, value):
+        """We accept all values not already given to other students.
+        """
+        cat = queryUtility(ICatalog, self.cat_name)
+        if cat is None:
+            return True
+        kw = {self.field_name: (value, value)}
+        results = cat.searchResults(**kw)
+        for entry in results:
+            if not hasattr(self.context, self.comp_field):
+                # we have no context with comp_field (most probably
+                # while adding a new object, where the container is
+                # the context) which means that the value was given
+                # already to another object (as _something_ was found in
+                # the catalog with that value). Fail on first round.
+                raise self.validation_error(value)
+            if getattr(entry, self.comp_field) != getattr(
+                self.context, self.comp_field):
+                # An entry already given to another student is not in our
+                # range of acceptable values.
+                raise self.validation_error(value)
+                #return False
+        return True
+
+def contextual_reg_num_source(context):
+    source = RegNumberSource(context)
+    return source
+directlyProvides(contextual_reg_num_source, IContextSourceBinder)
+
+class MatNumberSource(RegNumberSource):
+    field_name = 'matric_number'
+    validation_error = MatNumNotInSource
+
+def contextual_mat_num_source(context):
+    source = MatNumberSource(context)
+    return source
+directlyProvides(contextual_mat_num_source, IContextSourceBinder)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/webservices.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/webservices.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/webservices.py	(revision 16045)
@@ -0,0 +1,397 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+import grok
+import os
+import xmlrpclib
+from cStringIO import StringIO
+from zope.component import getUtility, queryUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import (
+    IUniversity, IExtFileStore, IFileStoreNameChooser, IKofaUtils
+    )
+from waeup.kofa.utils.helpers import get_fileformat, to_timezone
+from waeup.kofa.students.catalog import StudentsQuery
+from waeup.kofa.students.export import get_payments
+
+
+def get_student(students, identifier):
+    if identifier is None:
+        return None
+    student = students.get(identifier, None)
+    if student is None:
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(matric_number=(identifier, identifier)))
+        if len(results) == 1:
+            student = results[0]
+        else:
+            results = list(
+                cat.searchResults(reg_number=(identifier, identifier)))
+            if len(results) == 1:
+                student = results[0]
+    return student
+
+#class XMLRPCPermission(grok.Permission):
+#    """Permission for using XMLRPC functions.
+#    """
+#    grok.name('waeup.xmlrpc')
+
+#class XMLRPCUsers2(grok.Role):
+#    """Usergroup 2
+#    """
+#    grok.name('waeup.xmlrpcusers2')
+#    grok.title('XMLRPC Users Group 2')
+#    grok.permissions('waeup.xmlrpc',)
+
+
+class StudentsXMLRPC(grok.XMLRPC):
+    """Student related XMLRPC webservices.
+
+    Please note, that XMLRPC does not support real keyword arguments
+    but positional arguments only.
+    """
+
+    grok.context(IUniversity)
+
+    @grok.require('waeup.xmlrpc')
+    def get_student_id(self, reg_number=None):
+        """Get the id of a student with registration number `reg_number`.
+
+        Returns the student id as string if successful, ``None`` else.
+        """
+        if reg_number is not None:
+            cat = getUtility(ICatalog, name='students_catalog')
+            result = list(
+                cat.searchResults(reg_number=(reg_number, reg_number),
+                                  _limit=1))
+            if not len(result):
+                return None
+            return result[0].student_id
+        return None
+
+    @grok.require('waeup.xmlrpc')
+    def get_courses_by_session(self, identifier=None, session=None):
+        """1. What COURSES are registered by student X in session Y?
+
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return None
+        try:
+            session = int(session)
+        except (TypeError, ValueError):
+            pass
+        sessionsearch = True
+        if session in (None, ''):
+            sessionsearch = False
+        studycourse = student['studycourse']
+        coursetickets = {}
+        for level in studycourse.values():
+            if sessionsearch and level.level_session != session:
+                continue
+            for ct in level.values():
+                coursetickets.update(
+                    {"%s|%s" % (level.level, ct.code): ct.title})
+        if not coursetickets:
+            return None
+        return coursetickets
+
+    @grok.require('waeup.xmlrpc')
+    def get_students_by_course(self, course=None, session=None):
+        """2. What STUDENTS registered (student id / matric no)
+        for course Z in session Y and did they pay school fee?
+
+        """
+        try:
+            session = int(session)
+        except (TypeError, ValueError):
+            pass
+        sessionsearch = True
+        if session in (None, ''):
+            sessionsearch = False
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        if sessionsearch:
+            coursetickets = cat.searchResults(
+                session=(session, session),
+                code=(course, course))
+        else:
+            coursetickets = cat.searchResults(
+                code=(course, course))
+        if not coursetickets:
+            return None
+        hitlist = []
+        for c_ticket in coursetickets:
+            amount = 0
+            for p_ticket in c_ticket.student['payments'].values():
+                if p_ticket.p_state == 'paid' and \
+                    p_ticket.p_category == 'schoolfee' and \
+                    p_ticket.p_session == c_ticket.__parent__.level_session:
+                    amount = p_ticket.amount_auth
+            hitlist.append((
+                c_ticket.student.student_id,
+                c_ticket.student.matric_number,
+                c_ticket.__parent__.validated_by,
+                amount
+                ))
+        return list(set(hitlist))
+
+    @grok.require('waeup.xmlrpc')
+    def get_student_info(self, identifier=None):
+        """3a. Who is the student with matriculation number / student id
+
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return None
+        return [student.display_fullname, student.certcode,
+            student.phone, student.email]
+
+    @grok.require('waeup.xmlrpc')
+    def get_student_passport(self, identifier=None):
+        """3b. Get passport picture of student with
+        matriculation number / student id.
+
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return None
+        img = getUtility(IExtFileStore).getFileByContext(
+            student, attr='passport.jpg')
+        return xmlrpclib.Binary(img.read())
+
+    @grok.require('waeup.xmlrpc')
+    def get_paid_sessions(self, identifier=None):
+        """6. Get list of SESSIONS school fees paid by student X.
+
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return None
+        payments_dict = {}
+        for ticket in student['payments'].values():
+            if ticket.p_state == 'paid' and \
+                ticket.p_category == 'schoolfee':
+                payments_dict[str(ticket.p_session)] = ticket.amount_auth
+        if not payments_dict:
+            return None
+        return payments_dict
+
+    @grok.require('waeup.xmlrpc')
+    def check_student_credentials(self, username, password):
+        """Returns student data if username and password are valid,
+        None else.
+
+        We only query the students authenticator plugin in order not
+        to mix up with other credentials (admins, staff, etc.).
+
+        All additional checks performed by usual student
+        authentication apply. For instance for suspended students we
+        won't get a successful response but `None`.
+
+        This method can be used by CAS to authentify students for
+        external systems like moodle.
+        """
+        from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+        auth = getUtility(IAuthenticatorPlugin, name='students')
+        creds = dict(login=username, password=password)
+        principal = auth.authenticateCredentials(creds)
+        if principal is None:
+            return None
+        return dict(email=principal.email, id=principal.id,
+                    type=principal.user_type,
+                    description=principal.description)
+
+    @grok.require('waeup.xmlrpc')
+    def get_student_moodle_data(self, identifier=None):
+        """Returns student data to update user data and enroll user
+        in Moodle courses.
+
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return None
+        return dict(email=student.email,
+                    firstname=student.firstname,
+                    lastname=student.lastname,
+                    )
+
+    @grok.require('waeup.putBiometricData')
+    def put_student_fingerprints(self, identifier=None, fingerprints={}):
+        """Store fingerprint files for student identified by `identifier`.
+
+        `fingerprints` is expected to be a dict with strings
+        ``1``..``10`` as keys and binary data as values.
+
+        The keys ``1``..``10`` represent respective fingers: ``1`` is
+        the left thumb, ``10`` the little finger of right hand.
+
+        The binary data values are expected to be fingerprint minutiae
+        files as created by the libfprint library. With the std python
+        `xmlrpclib` client you can create such values with
+        `xmlrpclib.Binary(<BINARY_DATA_HERE>)`.
+
+        The following problems will raise errors:
+
+        - Invalid student identifiers (student does not exist or
+          unknown format of identifier)
+
+        - Fingerprint files that are not put into a dict.
+
+        - Fingerprint dicts that contain non-FPM files (or otherwise
+          invalid .fpm data).
+
+        Returns `True` in case of successful operation (at least one
+        fingerprint was stored), `False` otherwise.
+        """
+        result = False
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            raise xmlrpclib.Fault(
+                xmlrpclib.INVALID_METHOD_PARAMS,
+                "No such student: '%s'" % identifier)
+        if not isinstance(fingerprints, dict):
+            raise xmlrpclib.Fault(
+                xmlrpclib.INVALID_METHOD_PARAMS,
+                "Invalid fingerprint data: must be in dict")
+        for str_key, val in fingerprints.items():
+            num = 0
+            try:
+                num = int(str_key)
+            except ValueError:
+                pass
+            if num < 1 or num > 10:
+                continue
+            if not isinstance(val, xmlrpclib.Binary):
+                raise xmlrpclib.Fault(
+                    xmlrpclib.INVALID_METHOD_PARAMS,
+                    "Invalid data for finger %s" % num)
+            fmt = get_fileformat(None, val.data)
+            if fmt != 'fpm':
+                raise xmlrpclib.Fault(
+                    xmlrpclib.INVALID_METHOD_PARAMS,
+                    "Invalid file format for finger %s" % num)
+            file_store = getUtility(IExtFileStore)
+            file_id = IFileStoreNameChooser(student).chooseName(
+                attr='finger%s.fpm' % num)
+            file_store.createFile(file_id, StringIO(val.data))
+            student.writeLogMessage(self, 'fingerprint stored')
+            result = True
+        return result
+
+    @grok.require('waeup.getBiometricData')
+    def get_student_fingerprints(self, identifier=None):
+        """Returns student fingerprint data if available.
+
+        Result set is a dictionary with entries for ``email``,
+        ``firstname``, ``lastname``, ``img``, ``img_name``, and
+        ``fingerprints``.
+
+        Here ``img`` and ``img_name`` represent a photograph of the
+        student (binary image data and filename
+        respectively).
+
+        ``fingerprints`` is a dictionary itself with possible entries
+        ``1`` to ``10``, containing binary minutiae data
+        (i.e. fingerprint scans).
+        """
+        students = self.context['students']
+        student = get_student(students, identifier)
+        if student is None:
+            return dict()
+        result = dict(
+            email=student.email,
+            firstname=student.firstname,
+            lastname=student.lastname,
+            fingerprints={},
+            img_name=None,
+            img=None,
+            )
+        file_store = getUtility(IExtFileStore)
+        img = file_store.getFileByContext(student, attr='passport.jpg')
+        if img is not None:
+            result.update(
+                img=xmlrpclib.Binary(img.read()),
+                img_name=os.path.basename(img.name))
+
+        for num in [str(x + 1) for x in range(10)]:
+            fp_file = file_store.getFileByContext(
+                student, attr='finger%s.fpm' % num)
+            if fp_file is not None:
+                result['fingerprints'][num] = xmlrpclib.Binary(fp_file.read())
+        return result
+
+    @grok.require('waeup.xmlrpc')
+    def get_bursary_data(self,
+            current_session=None, current_level=None, certcode=None,
+            current_mode=None, depcode=None):
+        """Returns bursary data of a subset of students.
+        """
+        if not current_session:
+            current_session = None
+        if not current_level:
+            current_level = None
+        if not depcode:
+            depcode = None
+        if not certcode:
+            certcode = None
+        if not current_mode:
+            current_mode = None
+        hitlist = []
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = list(
+            cat.searchResults(
+                current_session=(current_session, current_session),
+                current_level=(current_level, current_level),
+                certcode=(certcode, certcode),
+                current_mode=(current_mode, current_mode),
+                depcode=(depcode, depcode),
+                ))
+        payments = get_payments(results)
+        tz = getUtility(IKofaUtils).tzinfo
+        for payment in payments:
+            hitlist.append(dict(
+                student_id=payment.student.student_id,
+                matric_number=payment.student.matric_number,
+                reg_number=payment.student.reg_number,
+                firstname=payment.student.firstname,
+                middlename=payment.student.middlename,
+                lastname=payment.student.lastname,
+                state=payment.student.state,
+                current_session=payment.student.current_session,
+                entry_session=payment.student.entry_session,
+                entry_mode=payment.student.entry_mode,
+                faccode=payment.student.faccode,
+                depcode=payment.student.depcode,
+                certcode=payment.student.certcode,
+                p_id=payment.p_id,
+                amount_auth=payment.amount_auth,
+                p_category=payment.p_category,
+                display_item=payment.display_item,
+                p_session=payment.p_session,
+                p_state=payment.p_state,
+                creation_date=str('%s#' % to_timezone(payment.creation_date, tz)),
+                payment_date=str('%s#' % to_timezone(payment.payment_date, tz)),
+                )
+              )
+        return hitlist
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/workflow.py	(revision 16045)
@@ -0,0 +1,332 @@
+## $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
+##
+"""Workflow for students.
+"""
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.kofa.interfaces import (
+    IObjectHistory, IKofaWorkflowInfo, IKofaUtils,
+    CREATED, ADMITTED, CLEARANCE, REQUESTED, CLEARED, PAID, RETURNING,
+    REGISTERED, VALIDATED, GRADUATED, TRANSREQ, TRANSVAL, TRANSREL,
+    IExtFileStore)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
+from waeup.kofa.students.interfaces import IStudent, IStudentsUtils
+from waeup.kofa.utils.helpers import get_current_principal
+
+
+IMPORTABLE_STATES = (ADMITTED, CLEARANCE, REQUESTED, CLEARED, PAID, RETURNING,
+    REGISTERED, VALIDATED, GRADUATED)
+
+FORBIDDEN_POSTGRAD_STATES = (RETURNING, REGISTERED, VALIDATED)
+
+ALUMNI_STATES = (GRADUATED, TRANSREQ, TRANSVAL, TRANSREL)
+
+REGISTRATION_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create student'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Record created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'admit',
+        title = _('Admit student'),
+        msg = _('Admitted'),
+        source = CREATED,
+        destination = ADMITTED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset student'),
+        msg = _('Reset to initial state'),
+        source = ADMITTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'start_clearance',
+        title = _('Start clearance'),
+        msg = _('Clearance started'),
+        source = ADMITTED,
+        destination = CLEARANCE),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset to admitted'),
+        msg = _("Reset to 'admitted'"),
+        source = CLEARANCE,
+        destination = ADMITTED),
+
+    Transition(
+        transition_id = 'request_clearance',
+        title = _('Request clearance'),
+        msg = _('Clearance requested'),
+        source = CLEARANCE,
+        destination = REQUESTED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset to clearance started'),
+        msg = _("Reset to 'clearance started'"),
+        source = REQUESTED,
+        destination = CLEARANCE),
+
+    Transition(
+        transition_id = 'clear',
+        title = _('Clear student'),
+        msg = _('Cleared'),
+        source = REQUESTED,
+        destination = CLEARED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset to clearance started'),
+        msg = _("Reset to 'clearance started'"),
+        source = CLEARED,
+        destination = CLEARANCE),
+
+    Transition(
+        transition_id = 'pay_first_school_fee',
+        title = _('Pay school fee'),
+        msg = _('First school fee payment made'),
+        source = CLEARED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'approve_first_school_fee',
+        title = _('Approve payment'),
+        msg = _('First school fee payment approved'),
+        source = CLEARED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset5',
+        title = _('Reset to cleared'),
+        msg = _("Reset to 'cleared'"),
+        source = PAID,
+        destination = CLEARED),
+
+    Transition(
+        transition_id = 'pay_school_fee',
+        title = _('Pay school fee'),
+        msg = _('School fee payment made'),
+        source = RETURNING,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'pay_pg_fee',
+        title = _('Pay PG school fee'),
+        msg = _('PG school fee payment made'),
+        source = PAID,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'approve_school_fee',
+        title = _('Approve school fee payment'),
+        msg = _('School fee payment approved'),
+        source = RETURNING,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'approve_pg_fee',
+        title = _('Approve PG school fee payment'),
+        msg = _('PG school fee payment approved'),
+        source = PAID,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset6',
+        title = _('Reset to returning'),
+        msg = _("Reset to 'returning'"),
+        source = PAID,
+        destination = RETURNING),
+
+    Transition(
+        transition_id = 'register_courses',
+        title = _('Register courses'),
+        msg = _('Courses registered'),
+        source = PAID,
+        destination = REGISTERED),
+
+    Transition(
+        transition_id = 'reset7',
+        title = _('Unregister courses'),
+        msg = _("Courses unregistered"),
+        source = REGISTERED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'validate_courses',
+        title = _('Validate courses'),
+        msg = _('Courses validated'),
+        source = REGISTERED,
+        destination = VALIDATED),
+
+    Transition(
+        transition_id = 'bypass_validation',
+        title = _('Return and bypass validation'),
+        msg = _("Course validation bypassed"),
+        source = REGISTERED,
+        destination = RETURNING),
+
+    Transition(
+        transition_id = 'reset8',
+        title = _('Reset to school fee paid'),
+        msg = _("Reset to 'school fee paid'"),
+        source = VALIDATED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'return',
+        title = _('Return'),
+        msg = _("Returned"),
+        source = VALIDATED,
+        destination = RETURNING),
+
+    Transition(
+        transition_id = 'reset9',
+        title = _('Reset to courses validated'),
+        msg = _("Reset to 'courses validated'"),
+        source = RETURNING,
+        destination = VALIDATED),
+
+    # There is no transition to graduated.
+
+    Transition(
+        transition_id = 'request_transcript',
+        title = _('Request transript'),
+        msg = _("Transcript requested"),
+        source = GRADUATED,
+        destination = TRANSREQ),
+
+    Transition(
+        transition_id = 'reset10',
+        title = _('Reject transcript request'),
+        msg = _("Transcript request rejected"),
+        source = TRANSREQ,
+        destination = GRADUATED),
+
+    Transition(
+        transition_id = 'validate_transcript',
+        title = _('Validate transcript'),
+        msg = _("Transcript validated"),
+        source = TRANSREQ,
+        destination = TRANSVAL),
+
+    Transition(
+        transition_id = 'release_transcript',
+        title = _('Release transcript'),
+        msg = _("Transcript released"),
+        source = TRANSVAL,
+        destination = TRANSREL),
+
+    Transition(
+        transition_id = 'reset11',
+        title = _('Reset to graduated'),
+        msg = _("Transcript process reset"),
+        source = TRANSREL,
+        destination = GRADUATED),
+    )
+
+
+IMPORTABLE_TRANSITIONS = [i.transition_id for i in REGISTRATION_TRANSITIONS]
+
+FORBIDDEN_POSTGRAD_TRANS = ['reset6', 'register_courses']
+
+registration_workflow = KofaWorkflow(REGISTRATION_TRANSITIONS)
+
+class RegistrationWorkflowState(WorkflowState, grok.Adapter):
+    """An adapter to adapt Student objects to workflow states.
+    """
+    grok.context(IStudent)
+    grok.provides(IWorkflowState)
+
+    state_key = 'wf.registration.state'
+    state_id = 'wf.registration.id'
+
+class RegistrationWorkflowInfo(KofaWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Student objects to workflow info objects.
+    """
+    grok.context(IStudent)
+    grok.provides(IKofaWorkflowInfo)
+
+    def __init__(self, context):
+        self.context = context
+        self.wf = registration_workflow
+
+@grok.subscribe(IStudent, IWorkflowTransitionEvent)
+def handle_student_transition_event(obj, event):
+    """Append message to student history and log file when transition happened.
+
+    Lock and unlock clearance form.
+    Trigger actions after school fee payment.
+    """
+
+    msg = event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    # School fee payment of returning students triggers the change of
+    # current session, current level, and current verdict
+    if event.transition.transition_id in (
+        'pay_school_fee', 'approve_school_fee'):
+        getUtility(IStudentsUtils).setReturningData(obj)
+    elif event.transition.transition_id in (
+        'pay_pg_fee', 'approve_pg_fee'):
+        new_session = obj['studycourse'].current_session + 1
+        obj['studycourse'].current_session = new_session
+    elif event.transition.transition_id == 'validate_courses':
+        current_level = obj['studycourse'].current_level
+        level_object = obj['studycourse'].get(str(current_level), None)
+        if level_object is not None:
+            user = get_current_principal()
+            if user is None:
+                usertitle = 'system'
+            else:
+                usertitle = getattr(user, 'public_name', None)
+                if not usertitle:
+                    usertitle = user.title
+            level_object.validated_by = usertitle
+            level_object.validation_date = datetime.utcnow()
+    elif event.transition.transition_id == 'clear':
+        obj.officer_comment = None
+    elif event.transition.transition_id == 'reset8':
+        current_level = obj['studycourse'].current_level
+        level_object = obj['studycourse'].get(str(current_level), None)
+        if level_object is not None:
+            level_object.validated_by = None
+            level_object.validation_date = None
+    elif event.transition.transition_id == 'reset11':
+        transcript_file = getUtility(IExtFileStore).getFileByContext(
+            obj, attr='final_transcript')
+        if transcript_file:
+            getUtility(IExtFileStore).deleteFileByContext(
+                obj, attr='final_transcript')
+        obj['studycourse'].transcript_comment = None
+        obj['studycourse'].transcript_signees = None
+    # In some tests we don't have a students container
+    try:
+        students_container = grok.getSite()['students']
+        students_container.logger.info('%s - %s' % (obj.student_id,msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/testing.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/testing.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/testing.py	(revision 16045)
@@ -0,0 +1,512 @@
+## $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
+##
+"""Testing support for :mod:`waeup.kofa`.
+"""
+import grok
+import doctest
+import logging
+import os.path
+import re
+import tempfile
+import shutil
+import unittest
+import warnings
+import zope.component
+import waeup.kofa
+from contextlib import contextmanager
+from zc.async.interfaces import COMPLETED
+from zope.app.testing.functional import (
+    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
+from zope.component import getGlobalSiteManager, queryUtility, getUtility
+from zope.security.testing import addCheckerPublic
+from zope.testing import renormalizing
+from zope.testing.cleanup import cleanUp
+from waeup.kofa.interfaces import IKofaUtils
+
+ftesting_zcml = os.path.join(
+    os.path.dirname(waeup.kofa.__file__), 'ftesting.zcml')
+FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
+                            allow_teardown=True)
+
+
+def get_all_loggers():
+    """Get the keys of all logger defined globally.
+    """
+    result = logging.root.manager.loggerDict.keys()
+    ws_loggers = [x for x in result if 'waeup.kofa' in x]
+    if ws_loggers:
+        # For debugging: show any remaining loggers from w.k. namespace
+        print "\nLOGGERS: ", ws_loggers
+    return result
+
+
+def remove_new_loggers(old_loggers):
+    """Remove the loggers except `old_loggers`.
+
+    `old_loggers` is a list of logger keys as returned by
+    :func:`get_all_loggers`. All globally registered loggers whose
+    name is not in `old_loggers` is removed.
+    """
+    new_loggers = [key for key in logging.root.manager.loggerDict
+                   if key not in old_loggers]
+    for key in sorted(new_loggers, reverse=True):
+        logger = logging.getLogger(key)
+        for handler in logger.handlers:
+            handler.close()
+        del logger
+        del logging.root.manager.loggerDict[key]
+    return
+
+
+def remove_logger(name):
+    """Remove logger with name `name`.
+
+    Use is safe. If the logger does not exist nothing happens.
+    """
+    if name in logging.root.manager.loggerDict.keys():
+        del logging.root.manager.loggerDict[name]
+    return
+
+
+def setUpZope(test=None):
+    """Initialize a Zope-compatible environment.
+
+    Currently, we only initialize the event machinery.
+    """
+    zope.component.eventtesting.setUp(test)
+
+
+def cleanUpZope(test=None):
+    """Clean up Zope-related registrations.
+
+    Cleans up all registrations and the like.
+    """
+    cleanUp()
+
+
+def maybe_grok():
+    """Try to grok the :mod:`waeup.kofa` package.
+
+    For many tests, even simple ones, we want the components defined
+    somewhere in the :mod:`waeup.kofa` package being registered. While
+    grokking the complete package can become expensive when done many
+    times, we only want to grok if it did not happen
+    before. Furthermore regrokking the whole package makes no sense if
+    done already.
+
+    :func:`maybe_grok` checks whether any eventhandlers are already
+    registered and does nothing in that case.
+
+    The grokking of :mod:`waeup.kofa` is done with warnings disabled.
+
+    Returns ``True`` if grokking was done, ``False`` else.
+
+    .. The following samples should go into Sphinx docs directly....
+
+    Sample
+    ******
+
+    Usage with plain Python testrunners
+    -----------------------------------
+
+    Together with the :func:`setUpZope` and :func:`cleanUpZope`
+    functions we then can do unittests with all components registered
+    and the event dispatcher in place like this::
+
+      import unittest2 as unittest # Want Python 2.7 features
+      from waeup.kofa.testing import (
+        maybe_grok, setUpZope, cleanUpZope,
+        )
+      from waeup.kofa.app import University
+
+      class MyTestCase(unittest.TestCase):
+
+          @classmethod
+          def setUpClass(cls):
+              grokked = maybe_grok()
+              if grokked:
+                  setUpZope(None)
+              return
+
+          @classmethod
+          def tearDownClass(cls):
+              cleanUpZope(None)
+
+          def setUp(self):
+              pass
+
+          def tearDown(self):
+              pass
+
+          def test_jambdata_in_site(self):
+              u = University()
+              self.assertTrue('jambdata' in u.keys())
+              return
+
+    Here the component registration is done only once for the whole
+    :class:`unittest.TestCase` and no ZODB is needed. That means
+    inside the tests you can expect to have all :mod:`waeup.kofa`
+    components (utilities, adapter, events) registered but as objects
+    have here still have no place inside a ZODB things like 'browsing'
+    won't work out of the box. The benefit is the faster test
+    setup/teardown.
+
+    .. note:: This works only with the default Python testrunners.
+
+         If you use the Zope testrunner (from :mod:`zope.testing`)
+         then you have to use appropriate layers like the
+         :class:`waeup.kofa.testing.KofaUnitTestLayer`.
+
+    Usage with :mod:`zope.testing` testrunners
+    ------------------------------------------
+
+    If you use the standard Zope testrunner, classmethods like
+    `setUpClass` are not executed. Instead you have to use a layer
+    like the one defined in this module.
+
+    .. seealso:: :class:`waeup.kofa.testing.KofaUnitTestLayer`
+
+    """
+    gsm = getGlobalSiteManager()
+    # If there are any event handlers registered already, we assume
+    # that waeup.kofa was grokked already. There might be a batter
+    # check, though.
+    if len(list(gsm.registeredHandlers())) > 0:
+        return False
+    # Register the zope.Public permission, normally done via ZCML setup.
+    addCheckerPublic()
+    warnings.simplefilter('ignore')   # disable (erraneous) warnings
+    grok.testing.grok('waeup.kofa')
+    warnings.simplefilter('default')  # reenable warnings
+    return True
+
+
+def setup_datacenter_conf():
+    """Register a datacenter config utility for non-functional tests.
+    """
+    from waeup.kofa.interfaces import IDataCenterConfig
+    conf = queryUtility(IDataCenterConfig)
+    if conf is not None:
+        return
+    path = tempfile.mkdtemp()
+    conf = {'path': path}
+    gsm = getGlobalSiteManager()
+    gsm.registerUtility(conf, IDataCenterConfig)
+    return
+
+
+def teardown_datacenter_conf():
+    """Unregister a datacenter config utility for non-functional tests.
+    """
+    from waeup.kofa.interfaces import IDataCenterConfig
+    conf = queryUtility(IDataCenterConfig)
+    if conf is None:
+        return
+    path = conf['path']
+    shutil.rmtree(path)
+    gsm = getGlobalSiteManager()
+    gsm.unregisterUtility(conf, IDataCenterConfig)
+    return
+
+
+class KofaUnitTestLayer(object):
+    """A layer for tests that groks `waeup.kofa`.
+
+    A Zope test layer that registers all :mod:`waeup.kofa` components
+    before attached tests are run and cleans this registrations up
+    afterwards. Also basic (non-waeup.kofa) components like the event
+    dispatcher machinery are registered, set up and cleaned up.
+
+    This layer does not provide a complete ZODB setup (and is
+    therefore much faster than complete functional setups) but does
+    only the registrations (which also takes some time, so running
+    this layer is slower than test cases that need none or only a
+    few registrations).
+
+    The registrations are done for all tests the layer is attached to
+    once before all these tests are run (and torn down once
+    afterwards).
+
+    To make use of this layer, you have to write a
+    :mod:`unittest.TestCase` class that provides an attribute called
+    ``layer`` with this class as value like this::
+
+      import unittest
+      from waeup.kofa.testing import KofaUnitTestLayer
+
+      class MyTestCase(unittest.TestCase):
+
+          layer = KofaUnitTestLayer
+
+          # per-test setups and real tests go here...
+          def test_foo(self):
+              self.assertEqual(1, 1)
+              return
+
+    """
+    @classmethod
+    def setUp(cls):
+        #setUpZope(None)
+        grokked = maybe_grok()
+        setup_datacenter_conf()
+        return
+
+    @classmethod
+    def tearDown(cls):
+        teardown_datacenter_conf()
+        cleanUpZope(None)
+        return
+
+
+#: This extended :class:`doctest.OutputChecker` allows the following
+#: additional matches when looking for output diffs:
+#:
+#: `N.NNN seconds`
+#:    matches strings like ``12.123 seconds``
+#:
+#: `HTTPError:`
+#:    matches ``httperror_seek_wrapper:``. This string is output by some
+#:    virtual browsers you might use in functional browser tests to signal
+#:    HTTP error state.
+#:
+#: `1034h`
+#:    is ignored. This sequence of control chars is output by some
+#:    (buggy) testrunners at beginning of output.
+#:
+#: `<10-DIGITS>`
+#:    matches a sequence of 10 digits. Useful when checking accesscode
+#:    numbers if you don't know the exact (random) code.
+#:
+#: `<6-DIGITS>`
+#:    matches a sequence of 6 digits. Useful when checking accesscode
+#:    numbers if you don't know the exact (random) code.
+#:
+#: `<YYYY-MM-DD hh:mm:ss>`
+#:    matches any date and time like `2011-05-01 12:01:32`.
+#:
+#: `<DATE-AND-TIME>`
+#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
+checker = renormalizing.RENormalizing([
+    # Relevant normalizers from zope.testing.testrunner.tests:
+    (re.compile(r'\d+[.]\d\d\d seconds'), 'N.NNN seconds'),
+    # Our own one to work around
+    # http://reinout.vanrees.org/weblog/2009/07/16/invisible-test-diff.html:
+    (re.compile(r'.*1034h'), ''),
+    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:'),
+    (re.compile('[\d]{6}'), '<6-DIGITS>'),
+    (re.compile('[\d]{10}'), '<10-DIGITS>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d [\+\-]\d\d\d\d [^ ]+'),
+     '<YYYY-MM-DD hh:mm:ss TZ>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
+    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
+    ])
+
+
+old_loggers = []
+
+
+def setUp(test):
+    global old_loggers
+    old_loggers = get_all_loggers()
+    FunctionalTestSetup().setUp()
+
+
+def tearDown(test):
+    global old_loggers
+    FunctionalTestSetup().tearDown()
+    remove_new_loggers(old_loggers)
+
+
+def doctestsuite_for_module(dotted_path):
+    """Create a doctest suite for the module at `dotted_path`.
+    """
+    test = doctest.DocTestSuite(
+        dotted_path,
+        setUp=setUp,
+        tearDown=tearDown,
+        checker=checker,
+        extraglobs=dict(
+            getRootFolder=getRootFolder,
+            sync=sync,),
+        optionflags=(doctest.ELLIPSIS +
+                     doctest.NORMALIZE_WHITESPACE +
+                     doctest.REPORT_NDIFF),
+        )
+    test.layer = FunctionalLayer
+    return test
+
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+
+def clear_logger_collector():
+    from zope.component import queryUtility
+    from waeup.kofa.interfaces import ILoggerCollector
+    collector = queryUtility(ILoggerCollector)
+    if collector is None:
+        return
+    keys = list(collector.keys())
+    for key in keys:
+        del collector[key]
+    return
+
+
+class FunctionalTestCase(FunctionalTestCase):
+    """A test case that supports checking output diffs in doctest style.
+    """
+
+    def setUp(self):
+        super(FunctionalTestCase, self).setUp()
+        self.functional_old_loggers = get_all_loggers()
+        return
+
+    def tearDown(self):
+        super(FunctionalTestCase, self).tearDown()
+        remove_new_loggers(self.functional_old_loggers)
+        clear_logger_collector()
+        return
+
+    def assertMatches(self, want, got, checker=checker,
+                      optionflags=optionflags):
+        """Assert that the multiline string `want` matches `got`.
+
+        In `want` you can use shortcuts like ``...`` as in regular doctests.
+
+        If no special `checker` is passed, we use an extended
+        :class:`doctest.OutputChecker` as defined in
+        :mod:`waeup.kofa.testing`.
+
+        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
+        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
+
+        .. seealso:: :data:`waeup.kofa.testing.optionflags`
+
+        .. seealso:: :data:`waeup.kofa.testing.checker`
+        """
+        if checker.check_output(want, got, optionflags):
+            return
+        diff = checker.output_difference(
+            doctest.Example('', want), got, optionflags)
+        self.fail(diff)
+
+
+class FunctionalTestSetup(FunctionalTestSetup):
+    """A replacement for the zope.app.testing class.
+
+    Removes also loggers.
+    """
+
+    def setUp(self):
+        self.old_loggers = get_all_loggers()
+        super(FunctionalTestSetup, self).setUp()
+        return
+
+    def tearDown(self):
+        super(FunctionalTestSetup, self).tearDown()
+        remove_new_loggers(self.old_loggers)
+        return
+
+
+def get_doctest_suite(filename_list=[]):
+    """Helper function to create doctest suites for doctests.
+
+    The `filename_list` is a list of filenames relative to the
+    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
+    ``blah.txt`` in the ``browser/`` subpackage you have to pass
+    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
+    so on.
+
+    The returned test suite must be registered somewhere locally for
+    instance by something like:
+
+      from waeup.kofa.testing import get_doctest_suite
+      def test_suite():
+        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
+        return suite
+
+    and that's it.
+    """
+    suite = unittest.TestSuite()
+    for filename in filename_list:
+        path = os.path.join(
+            os.path.dirname(__file__), filename)
+        test = doctest.DocFileSuite(
+            path,
+            module_relative=False,
+            setUp=setUp, tearDown=tearDown,
+            globs=dict(getRootFolder=getRootFolder),
+            optionflags=doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
+            checker=checker,
+            )
+        test.layer = FunctionalLayer
+        suite.addTest(test)
+    return suite
+
+
+class FakeJob(object):
+    # A job usable for simple async tests
+    status = COMPLETED
+    result = None
+    acts_under_heavy_load = False  # set to True to mimic system high load
+
+    def __init__(self, *args, **kw):
+        self.args = args
+        self.kwargs = kw
+
+
+class FakeJobManager(object):
+    # A fake job manager for testing async functionality
+
+    def __init__(self):
+        # make sure each instance maintains an own set of jobs/nums.
+        self._jobs = dict()
+        self._curr_num = 1
+
+    def get(self, job_id):
+        if job_id == '3':
+            return FakeJob()
+        return self._jobs.get(job_id, None)
+
+    def put(self, job):
+        if getattr(job, 'acts_under_heavy_load', False):
+            return None
+        num = str(self._curr_num)
+        self._jobs[num] = job
+        self._curr_num += 1
+        return num
+
+    def remove(self, job_id, site):
+        if job_id in self._jobs:
+            del self._jobs[job_id]
+        return
+
+
+@contextmanager
+def expensive_actions():
+    """A contextmanager to work with a modified `expensive_acrtions_allowed()`
+
+    This method is part of local IKofaUtils.
+    """
+    utils = getUtility(IKofaUtils)
+    old_func = utils.expensive_actions_allowed
+    try:
+        yield utils
+    finally:
+        # restore function after use
+        utils.expensive_actions_allowed = old_func
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/__init__.py	(revision 16045)
@@ -0,0 +1,2 @@
+# This is a package
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_app.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_app.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_app.py	(revision 16045)
@@ -0,0 +1,71 @@
+## $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
+##
+import tempfile
+import shutil
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import queryUtility
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import (
+    IUniversity, IJobManager, VIRT_JOBS_CONTAINER_NAME)
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+class UniversityTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UniversityTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.getRootFolder()['app'] = University()
+        self.app = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(UniversityTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        university = University()
+        assert verifyClass(IUniversity, University)
+        assert verifyObject(IUniversity, university)
+        return
+
+    def test_IFileRetrieval_utility(self):
+        # Make sure we can get a local IFileRetrieval utility
+        setSite(self.app)
+        result = queryUtility(IFileRetrieval, default=None)
+        assert result is not None
+        assert IFileRetrieval.providedBy(result)
+        return
+
+    def disabled_test_update_plugins(self):
+        # We can update plugins
+        setSite(self.app)
+        del self.app['documents']
+        self.app.updatePlugins()
+        self.assertTrue('documents' in self.app.keys())
+        return
+
+    def test_jobs(self):
+        # We can get the global job manager when traversing to it...
+        result = self.app.traverse(VIRT_JOBS_CONTAINER_NAME)
+        self.assertTrue(IJobManager.providedBy(result))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_async.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_async.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_async.py	(revision 16045)
@@ -0,0 +1,411 @@
+# Tests for the async module of waeup.kofa
+import os
+import re
+import shutil
+import tempfile
+import transaction
+import zc.async.configure
+import zc.async.ftesting
+import zc.async.queue
+from zc.async.interfaces import IJob
+from ZODB import DB, FileStorage
+from zope.component import getUtility, provideAdapter
+from zope.component.hooks import setSite
+from zope.container.interfaces import IContainer
+from zope.interface import verify
+from zope.site.folder import rootFolder
+from zope.site.testing import createSiteManager
+from waeup.kofa.interfaces import (
+    WAEUP_KEY, IProgressable, IJobManager, IJobContainer
+    )
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.async import (
+    get_job_id, AsyncJob, ProgressableJob, JobManager, JobContainer,
+    JobManagerTraverser, JobContainerInstaller,
+    )
+
+def fake_async_func1():
+    return 23
+
+def fake_async_func2():
+    return 42
+
+class FunctionalAsyncTestCase(FunctionalTestCase):
+    """A functional test case that additionally sets up asynchronous
+    machinery.
+
+    These type of test case takes _much_ more time than regular
+    functional tests! So, only use it if really neccessary.
+    """
+
+    def setUp(self):
+        super(FunctionalAsyncTestCase, self).setUp()
+        zc.async.configure.base()
+        provideAdapter(zc.async.queue.getDefaultQueue)
+        connection = self.getRootFolder()._p_jar
+        zc.async.ftesting.setUp(connection=connection)
+        return
+
+    def tearDown(self):
+        zc.async.ftesting.tearDown()
+        super(FunctionalAsyncTestCase, self).tearDown()
+        return
+
+class CommonAsyncFunctionalTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface_async_job(self):
+        # AsyncJob implements the promised interfaces
+        job = AsyncJob(fake_async_func1)
+        verify.verifyClass(IJob, AsyncJob)
+        verify.verifyObject(IJob, job)
+        return
+
+    def test_iface_progressable_job(self):
+        # ProgressableJob implements the promised interfaces
+        job = ProgressableJob(fake_async_func1)
+        verify.verifyClass(IJob, ProgressableJob)
+        verify.verifyClass(IProgressable, ProgressableJob)
+        verify.verifyObject(IJob, job)
+        verify.verifyObject(IProgressable, job)
+        return
+
+    def test_get_job_id(self):
+        # for persisted jobs we can get an oid as job id
+        job = AsyncJob(fake_async_func1)
+        self.getRootFolder()['myjob'] = job
+        transaction.commit()
+        oid = job.__repr__().split(' ')[2][:-1]
+        result = get_job_id(job)
+        self.assertEqual(result, oid)
+        return
+
+class JobContainerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface_job_container(self):
+        # JobContainers implement the promised interfaces
+        container = JobContainer()
+        verify.verifyClass(IContainer, JobContainer)
+        verify.verifyObject(IContainer, container)
+        verify.verifyClass(IJobContainer, JobContainer)
+        verify.verifyObject(IJobContainer, container)
+        return
+
+class JobManagerTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobManagerTests, self).setUp()
+        self.root_folder = self.getRootFolder()
+        self.root = self.root_folder._p_jar.root()
+
+    def set_site(self):
+        # helper to set a site 'externally'.
+        # This is a bit complicated under the given limited setup conditions.
+        # Returns a persisted site.
+        site = rootFolder()
+        self.root_folder['application'] = site
+        createSiteManager(site, setsite=True)
+        transaction.commit()
+        return site
+
+    def test_iface_job_manager(self):
+        # JobManager implements the promised interfaces
+        manager = JobManager()
+        verify.verifyClass(IJobManager, JobManager)
+        verify.verifyObject(IJobManager, manager)
+        return
+
+    def test_get_job_manager_as_util(self):
+        # we can get a job manager as global, unnamed utility
+        manager = getUtility(IJobManager)
+        self.assertTrue(isinstance(manager, JobManager))
+        return
+
+    def test_get_site_no_site(self):
+        # if no site is around, we get a lookup error
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager._get_site, None)
+        return
+
+    def test_get_site_invalid_site(self):
+        # if we pass an invalid site, we get a lookup error
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager._get_site, object())
+        return
+
+    def test_get_site_valid_site(self):
+        # if we pass a valid site, that will be respected
+        manager = getUtility(IJobManager)
+        result = manager._get_site(self.root_folder)
+        self.assertEqual(result, self.root_folder)
+        return
+
+    def test_get_site_ext_set_none_given(self):
+        # with an externally set site we don't have to pass in a site
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(None)
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_valid_site(self):
+        # with an externally set site we can pass in this site
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(site)
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_invalid_site(self):
+        # with an externally set site we can pass in invalid sites
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(object())
+        self.assertEqual(result, site)
+        return
+
+    def test_get_site_ext_set_valid_obj(self):
+        # valid persisted objects override externally set sites
+        manager = getUtility(IJobManager)
+        site = self.set_site()
+        result = manager._get_site(self.root_folder)
+        self.assertEqual(result, self.root_folder)
+        return
+
+    def test_put(self):
+        # we can put jobs into the manager
+        myjob = AsyncJob(fake_async_func1)
+        manager = getUtility(IJobManager)
+        job_id = manager.put(myjob, site=self.root_folder)
+        # the returned job_id should be a string with a bunch of numbers
+        self.assertTrue(re.match('^\d+$', job_id))
+        # the job is also in the (new) jobs container
+        self.assertTrue(job_id in self.root[WAEUP_KEY].keys())
+        # the job is really stored
+        stored_job =self.root[WAEUP_KEY][job_id]
+        self.assertEqual(myjob, stored_job)
+        return
+
+    def test_put_no_site(self):
+        # if no site is around we cannot store jobs
+        myjob = AsyncJob(fake_async_func1)
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager.put, myjob)
+        return
+
+    def test_get(self):
+        # we can get a job back with a valid job_id
+        manager = getUtility(IJobManager)
+        myjob = AsyncJob(fake_async_func1)
+        job_id = manager.put(myjob, site=self.root_folder)
+        stored_job = manager.get(job_id, site=self.root_folder)
+        self.assertEqual(myjob, stored_job)
+        return
+
+    def test_get_no_site(self):
+        # if no site is around we cannot retrieve jobs
+        manager = getUtility(IJobManager)
+        self.assertRaises(LookupError, manager.get, 'some_id')
+        return
+
+    def test_get_invalid_jobid(self):
+        # invalid job_ids will result in `None` (if site is valid)
+        manager = getUtility(IJobManager)
+        result = manager.get('not-a-valid-job-id', site=self.root_folder)
+        self.assertEqual(result, None)
+        return
+
+    def test_jobs(self):
+        # we can get all jobs contained
+        manager = getUtility(IJobManager)
+        myjob1 = AsyncJob(fake_async_func1)
+        job_id1 = manager.put(myjob1, site=self.root_folder)
+        myjob2 = AsyncJob(fake_async_func2)
+        job_id2 = manager.put(myjob2, site=self.root_folder)
+        result = sorted(list(manager.jobs(site=self.root_folder)))
+        self.assertEqual(
+            result,
+            [(job_id1, myjob1), (job_id2, myjob2)]
+            )
+        return
+
+    def test_remove(self):
+        # we can remove jobs from the job container
+        manager = getUtility(IJobManager)
+        myjob = AsyncJob(fake_async_func1)
+        job_id = manager.put(myjob, site=self.root_folder)
+        container = self.root[WAEUP_KEY]
+        self.assertEqual(len(container), 1)
+        manager.remove(job_id, site=self.root_folder)
+        self.assertEqual(len(container), 0)
+        return
+
+    def test_start_test_job(self):
+        # we can start a test job
+        manager = getUtility(IJobManager)
+        job_id = manager.start_test_job(duration=1, site=self.root_folder)
+        container = self.root[WAEUP_KEY]
+        self.assertEqual(len(container), 1)
+        self.assertTrue(job_id is not None)
+        return
+
+class JobManagerTraverserTests(FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobManagerTraverserTests, self).setUp()
+        self.root_folder = self.getRootFolder()
+        self.root = self.root_folder._p_jar.root()
+        self.manager = getUtility(IJobManager)
+        setSite(self.getRootFolder())
+
+    def tearDown(self):
+        setSite(None)
+        super(JobManagerTraverserTests, self).tearDown()
+
+    def test_no_jobs(self):
+        # the traverser returns None if no job is available
+        traverser = JobManagerTraverser(self.manager, None)
+        result = traverser.traverse('123')
+        self.assertTrue(result is None)
+        return
+
+    def test_valid_job(self):
+        # the traverser returns the appropriate job if available
+        myjob = AsyncJob(fake_async_func1)
+        job_id = self.manager.put(myjob, site=self.root_folder)
+        traverser = JobManagerTraverser(self.manager, None)
+        result = traverser.traverse(job_id)
+        self.assertEqual(myjob, result)
+        return
+
+class FakeEvent(object):
+    # A faked DatabaseOpenedEvent as triggered at instance startup.
+    # The only attribute we need here is `database` which should
+    # return a valid ZODB.
+
+    _storage = None
+    _db = None
+
+    def __init__(self, workdir, db_name=None):
+        # workdir is the place to create a ZODB in, db_name its name.
+        self.workdir = workdir
+        self.db_name = db_name
+
+    @property
+    def database(self):
+        if self._storage is None:
+            # storage not created yet. Do it now.
+            path = os.path.join(self.workdir, 'testdb.fs')
+            self._storage = FileStorage.FileStorage(path)
+            self._db = DB(self._storage, database_name=self.db_name)
+        return self._db
+
+class FakeDB(object):
+
+    closed = False
+
+    def open(self, transaction_manager=None):
+        return self
+
+    def close(self):
+        self.closed = True
+        return
+
+class JobContainerInstallerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(JobContainerInstallerTests, self).setUp()
+        # setup two ZODBs, one unnamed, one named...
+        self.workdir1 = tempfile.mkdtemp()
+        self.workdir2 = tempfile.mkdtemp()
+        self.fake_event1 = FakeEvent(self.workdir1)
+        self.fake_event2 = FakeEvent(self.workdir2, 'mytestdb')
+        self.tm1 = transaction.TransactionManager()
+        self.tm2 = transaction.TransactionManager()
+        self.db1 = self.fake_event1.database
+        self.db2 = self.fake_event2.database
+        self.conn1 = self.db1.open(self.tm1)
+        self.conn2 = self.db2.open(self.tm2)
+
+    def tearDown(self):
+        self.tm1.abort()
+        self.tm2.abort()
+        shutil.rmtree(self.workdir1)
+        shutil.rmtree(self.workdir2)
+        super(JobContainerInstallerTests, self).tearDown()
+
+    def reopen(self):
+        # reopen connections to the ZODBs (for retrieving fresh data)...
+        self.conn1.close()
+        self.conn2.close()
+        self.conn1 = self.db1.open(self.tm1)
+        self.conn2 = self.db2.open(self.tm2)
+        return
+
+    @property
+    def root1(self):
+        # get root of first (unnamed) database
+        self.reopen()
+        return self.conn1.root()
+
+    @property
+    def root2(self):
+        # get root of second (named 'mytestdb') database
+        self.reopen()
+        return self.conn2.root()
+
+    def test_new_container(self):
+        # we can install a job container if none is available yet
+        installer = JobContainerInstaller()
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(IJobContainer.providedBy(self.root1[WAEUP_KEY]))
+        return
+
+    def test_existing_container(self):
+        # we won't try to install a new container if one is already present
+        installer = JobContainerInstaller()
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(IJobContainer.providedBy(self.root1[WAEUP_KEY]))
+        container = self.root1[WAEUP_KEY]
+        installer(self.fake_event1)
+        self.assertTrue(WAEUP_KEY in self.root1)
+        self.assertTrue(self.root1[WAEUP_KEY] is container)
+        return
+
+    def test_named_db(self):
+        # we can also install containers in named dbs
+        installer = JobContainerInstaller(db_name='mytestdb')
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        self.assertTrue(WAEUP_KEY not in self.root2)
+        installer(self.fake_event2)
+        # now the job container is in database2 (but not database1)
+        self.assertTrue(WAEUP_KEY not in self.root1)
+        self.assertTrue(WAEUP_KEY in self.root2)
+        self.assertTrue(IJobContainer.providedBy(self.root2[WAEUP_KEY]))
+        return
+
+    def test_broken_db(self):
+        # if writing to db fails, we close the connection anyway...
+        installer = JobContainerInstaller()
+        self.conn1.close()
+        # create a fake db
+        db = FakeDB()
+        self.fake_event1._storage = 'dummy'
+        self.fake_event1._db = db
+        self.assertEqual(db.closed, False)
+        self.assertRaises(AttributeError, installer, self.fake_event1)
+        self.assertEqual(db.closed, True)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_authentication.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_authentication.py	(revision 16045)
@@ -0,0 +1,457 @@
+## $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
+##
+import grok
+import logging
+import time
+import unittest
+from cStringIO import StringIO
+from zope.component import getGlobalSiteManager, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.testing import setUpPasswordManagers
+from zope.pluggableauth import PluggableAuthentication
+from zope.pluggableauth.interfaces import (
+    IAuthenticatorPlugin, ICredentialsPlugin)
+from zope.publisher.browser import TestRequest
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.app import University
+from waeup.kofa.authentication import (
+    UserAuthenticatorPlugin, Account, KofaPrincipalInfo, FailedLoginInfo,
+    get_principal_role_manager, UsersPlugin, KofaXMLRPCCredentialsPlugin,
+    setup_authentication, UpdatePAUPlugin)
+from waeup.kofa.interfaces import (
+    IAuthPluginUtility, IUserAccount, IFailedLoginInfo, IKofaPrincipalInfo,
+    IKofaPluggable)
+
+SECRET = 'HgtuZZZ8'
+
+class FakeSite(grok.Site, grok.Container):
+    #def getSiteManager(self):
+    #    return None
+    #    return getGlobalSiteManager()
+    pass
+
+
+class FakeAuthPlugin(object):
+    def register(self, pau):
+        pau.credentialsPlugins += ('foo', )
+
+
+class Test_setup_authentication(FunctionalTestCase):
+    # Tests for the `setup_authentication` function
+
+    layer = FunctionalLayer
+
+    def tearDown(self):
+        # clean up registry.
+        gsm = getGlobalSiteManager()
+        for iface, name in ((IAuthPluginUtility, 'myauth'), ):
+            to_delete = queryUtility(iface, name=name)
+            if to_delete is not None:
+                gsm.unregisterUtility(provided=iface, name=name)
+        super(Test_setup_authentication, self).tearDown()
+
+    def test_plugins_are_registered(self):
+        # We can populate a PAU with (hardcoded set of) plugins
+        pau = PluggableAuthentication()
+        setup_authentication(pau)
+        for name in (
+                'No Challenge if Authenticated',
+                'xmlrpc-credentials',
+                'credentials'):
+            assert name in pau.credentialsPlugins
+        for name in ('users', ):
+            assert name in pau.authenticatorPlugins
+
+    def test_external_plugins_are_registered(self):
+        # registered plugins are called as well
+        gsm = getGlobalSiteManager()
+        gsm.registerUtility(
+            FakeAuthPlugin(), IAuthPluginUtility, name='myauth')
+        pau = PluggableAuthentication()
+        setup_authentication(pau)
+        assert 'foo' in pau.credentialsPlugins
+
+
+class KofaXMLRPCCredentialsPluginTests(FunctionalTestCase):
+    # Test for XMLRPC credentials plugin
+
+    layer = FunctionalLayer
+
+    def test_ifaces(self):
+        # we meet interface requirements
+        plugin = KofaXMLRPCCredentialsPlugin()
+        self.assertTrue(
+            verifyClass(ICredentialsPlugin, KofaXMLRPCCredentialsPlugin))
+
+    def test_util_is_registered(self):
+        # we can query this named utility
+        util = queryUtility(ICredentialsPlugin, name='xmlrpc-credentials')
+        assert util is not None
+
+    def test_can_extract_creds(self):
+        # we can extract credentials from appropriate requests
+        req = TestRequest(
+            environ={'HTTP_AUTHORIZATION': u'Basic bWdyOm1ncnB3'})
+        plugin = KofaXMLRPCCredentialsPlugin()
+        assert plugin.extractCredentials(req) == {
+            'login': 'mgr', 'password': 'mgrpw'}
+
+    def test_challenge_disabled(self):
+        # we will not challenge people
+        plugin = KofaXMLRPCCredentialsPlugin()
+        assert plugin.challenge(TestRequest()) is False
+
+    def test_logout_disabled(self):
+        # we do not support logging out. HTTP basic auth cannot do this.
+        plugin = KofaXMLRPCCredentialsPlugin()
+        assert plugin.logout(TestRequest()) is False
+
+
+class UserAuthenticatorPluginTests(FunctionalTestCase):
+    # Must be functional because of various utility lookups and the like
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UserAuthenticatorPluginTests, self).setUp()
+        self.getRootFolder()['app'] = FakeSite()
+        self.site = self.getRootFolder()['app']
+        self.site['users'] = {'bob': Account('bob', SECRET)}
+        setSite(self.site)
+        return
+
+    def tearDown(self):
+        super(UserAuthenticatorPluginTests, self).tearDown()
+        clearSite()
+        return
+
+    def test_ifaces(self):
+        # make sure, interfaces requirements are met
+        plugin = UserAuthenticatorPlugin()
+        plugin.__parent__ = None # This attribute is required by iface
+        self.assertTrue(
+            verifyClass(IAuthenticatorPlugin, UserAuthenticatorPlugin))
+        self.assertTrue(verifyObject(IAuthenticatorPlugin, plugin))
+        return
+
+    def test_authenticate_credentials(self):
+        # make sure authentication works as expected
+        plugin = UserAuthenticatorPlugin()
+        result1 = plugin.authenticateCredentials(
+            dict(login='bob', password=SECRET))
+        result2 = plugin.authenticateCredentials(
+            dict(login='bob', password='nonsense'))
+        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
+        self.assertTrue(result2 is None)
+        return
+
+    def test_principal_info(self):
+        # make sure we can get a principal info
+        plugin = UserAuthenticatorPlugin()
+        result1 = plugin.principalInfo('bob')
+        result2 = plugin.principalInfo('manfred')
+        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
+        self.assertTrue(result2 is None)
+        return
+
+    def test_get_principal_role_manager(self):
+        # make sure we get different role managers for different situations
+        prm1 = get_principal_role_manager()
+        clearSite(None)
+        prm2 = get_principal_role_manager()
+        self.assertTrue(IPrincipalRoleManager.providedBy(prm1))
+        self.assertTrue(IPrincipalRoleManager.providedBy(prm2))
+        self.assertTrue(prm1._context is self.site)
+        self.assertTrue(hasattr(prm2, '_context') is False)
+        return
+
+    def make_failed_logins(self, num):
+        # do `num` failed logins and a valid one afterwards
+        del self.site['users']
+        self.site['users'] = {'bob': Account('bob', SECRET)}
+        plugin = UserAuthenticatorPlugin()
+        resultlist = []
+        # reset accounts
+        for x in range(num):
+            resultlist.append(plugin.authenticateCredentials(
+                dict(login='bob', password='wrongsecret')))
+        resultlist.append(plugin.authenticateCredentials(
+            dict(login='bob', password=SECRET)))
+        return resultlist
+
+    def DISABLED_test_failed_logins(self):
+        # after three failed logins, an account is blocked
+        # XXX: this tests authenticator with time penalty (currently
+        # disabled)
+        results = []
+        succ_principal = KofaPrincipalInfo(
+            id='bob',
+            title='bob',
+            description=None,
+            email=None,
+            phone=None,
+            public_name=None,
+            user_type=u'user')
+        for x in range(4):
+            results.append(self.make_failed_logins(x))
+        self.assertEqual(results[2], [None, None, succ_principal])
+        # last login was blocked although correctly entered due to
+        # time penalty
+        self.assertEqual(results[3], [None, None, None, None])
+        return
+
+class KofaPrincipalInfoTests(unittest.TestCase):
+
+    def create_info(self):
+        return KofaPrincipalInfo(
+            id='bob',
+            title='bob',
+            description=None,
+            email=None,
+            phone=None,
+            public_name=None,
+            user_type=u'user')
+
+    def test_iface(self):
+        # make sure we implement the promised interfaces
+        info = self.create_info()
+        verifyClass(IKofaPrincipalInfo, KofaPrincipalInfo)
+        verifyObject(IKofaPrincipalInfo, info)
+        return
+
+    def test_equality(self):
+        # we can test two infos for equality
+        info1 = self.create_info()
+        info2 = self.create_info()
+        self.assertEqual(info1, info2)
+        self.assertTrue(info1 == info2)
+        info1.id = 'blah'
+        self.assertTrue(info1 != info2)
+        self.assertTrue((info1 == info2) is False)
+        info1.id = 'bob'
+        info2.id = 'blah'
+        self.assertTrue(info1 != info2)
+        self.assertTrue((info1 == info2) is False)
+        return
+
+class FailedLoginInfoTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill the promised interfaces
+        info1 = FailedLoginInfo()
+        info2 = FailedLoginInfo(num=1, last=time.time())
+        self.assertTrue(
+            verifyClass(IFailedLoginInfo, FailedLoginInfo))
+        self.assertTrue(verifyObject(IFailedLoginInfo, info1))
+        # make sure the stored values have correct type if not None
+        self.assertTrue(verifyObject(IFailedLoginInfo, info2))
+        return
+
+    def test_default_values(self):
+        # By default we get 0, None
+        info = FailedLoginInfo()
+        self.assertEqual(info.num, 0)
+        self.assertEqual(info.last, None)
+        return
+
+    def test_set_values_by_attribute(self):
+        # we can set values by attribute
+        ts = time.gmtime(0)
+        info = FailedLoginInfo()
+        info.num = 5
+        info.last = ts
+        self.assertEqual(info.num, 5)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_set_values_by_constructor(self):
+        # we can set values by constructor args
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(5, ts)
+        self.assertEqual(info.num, 5)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_set_values_by_keywords(self):
+        # we can set values by constructor keywords
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(last=ts, num=3)
+        self.assertEqual(info.num, 3)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_as_tuple(self):
+        # we can get the info values as tuple
+        ts = time.gmtime(0)
+        info = FailedLoginInfo(last=ts, num=3)
+        self.assertEqual(info.as_tuple(), (3, ts))
+        return
+
+    def test_set_values(self):
+        # we can set the values of a an info instance
+        ts = time.time()
+        info = FailedLoginInfo()
+        info.set_values(num=3, last=ts)
+        self.assertEqual(info.num, 3)
+        self.assertEqual(info.last, ts)
+        return
+
+    def test_increase(self):
+        # we can increase the number of failed logins
+        ts1 = time.time()
+        info = FailedLoginInfo()
+        info.increase()
+        self.assertEqual(info.num, 1)
+        self.assertTrue(info.last > ts1)
+        ts2 = info.last
+        info.increase()
+        self.assertEqual(info.num, 2)
+        self.assertTrue(info.last > ts2)
+        return
+
+    def test_reset(self):
+        # we can reset failed login infos.
+        info = FailedLoginInfo()
+        info.increase()
+        info.reset()
+        self.assertEqual(info.num, 0)
+        self.assertEqual(info.last, None)
+        return
+
+class AccountTests(unittest.TestCase):
+
+    def setUp(self):
+        setUpPasswordManagers()
+        return
+
+    def test_iface(self):
+        acct = Account('bob', 'mypasswd')
+        self.assertTrue(
+            verifyClass(IUserAccount, Account))
+        self.assertTrue(
+            verifyObject(IUserAccount, acct))
+        return
+
+    def test_failed_logins(self):
+        # we can retrieve infos about failed logins
+        ts = time.time()
+        acct = Account('bob', 'mypasswd')
+        self.assertTrue(hasattr(acct, 'failed_logins'))
+        acct.failed_logins.set_values(num=3, last=ts)
+        self.assertEqual(acct.failed_logins.last, ts)
+        self.assertEqual(acct.failed_logins.num, 3)
+        return
+
+    def test_failed_logins_per_inst(self):
+        # we get a different counter for each Account instance
+        acct1 = Account('bob', 'secret')
+        acct2 = Account('alice', 'alsosecret')
+        self.assertTrue(acct1.failed_logins is not acct2.failed_logins)
+        return
+
+class FakeUserAccount(object):
+    pass
+
+
+def get_logger():
+    logger = logging.getLogger('waeup.test')
+    stream = StringIO()
+    handler = logging.StreamHandler(stream)
+    logger.setLevel(logging.DEBUG)
+    logger.propagate = False
+    logger.addHandler(handler)
+    return logger, stream
+
+
+class UsersPluginTests(unittest.TestCase):
+
+    def setUp(self):
+        setUpPasswordManagers()
+        self.site = FakeSite()
+        self.site['users'] = grok.Container()
+        return
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        plugin = UsersPlugin()
+        verifyClass(IKofaPluggable, UsersPlugin)
+        verifyObject(IKofaPluggable, plugin)
+        return
+
+    def test_update(self):
+        # make sure user accounts are updated properly.
+        plugin = UsersPlugin()
+        logger, stream = get_logger()
+        plugin.update(self.site, 'app', logger)
+        stream.seek(0)
+        self.assertEqual(stream.read(), '')
+        self.site['users']['bob'] = FakeUserAccount()
+        logger, stream = get_logger()
+        plugin.update(self.site, 'app', logger)
+        stream.seek(0)
+        log_content = stream.read()
+        self.assertTrue(hasattr(self.site['users']['bob'], 'description'))
+        self.assertTrue(hasattr(self.site['users']['bob'], 'failed_logins'))
+        self.assertTrue(
+            isinstance(self.site['users']['bob'].failed_logins,
+                       FailedLoginInfo))
+        self.assertTrue('attribute description added' in log_content)
+        self.assertTrue('attribute failed_logins added' in log_content)
+        return
+
+
+class TestUpdatePAUPlugin(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(TestUpdatePAUPlugin, self).setUp()
+        self.getRootFolder()['app'] = University()
+        self.site = self.getRootFolder()['app']
+
+    def tearDown(self):
+        clearSite()
+        super(TestUpdatePAUPlugin, self).tearDown()
+
+    def get_pau(self):
+        # the PAU is registered as a local utility in local site manager.
+        # the name is derived from class name.
+        pau = self.site.getSiteManager()['PluggableAuthentication']
+        assert pau is not None
+        return pau
+
+    def test_update_outdated(self):
+        # we can update outdated sites.
+        plugin = UpdatePAUPlugin()
+        logger, stream = get_logger()
+        pau = self.get_pau()
+        pau.credentialsPlugins = ('foo', 'credentials', 'bar')
+        plugin.update(self.site, 'xmlrpc-credentials', logger)
+        assert 'xmlrpc-credentials' in pau.credentialsPlugins
+        assert pau.credentialsPlugins.index('xmlrpc-credentials') == 1
+
+    def test_update_uptodate(self):
+        # we cope with already updated sites.
+        plugin = UpdatePAUPlugin()
+        logger, stream = get_logger()
+        pau = self.get_pau()
+        pau.credentialsPlugins = ('foo', 'xmlrpc-credentials', 'bar')
+        plugin.update(self.site, 'xmlrpc-credentials', logger)
+        assert pau.credentialsPlugins.count('xmlrpc-credentials') == 1
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_beaker.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_beaker.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_beaker.py	(revision 16045)
@@ -0,0 +1,22 @@
+from zope.component.hooks import setSite
+from zope.publisher.browser import TestRequest
+from zope.session.interfaces import ISession
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+class BeakerTests(FunctionalTestCase):
+    # Beaker-related tests.
+
+    layer = FunctionalLayer
+
+    def test_beaker_session(self):
+        # Make sure we get a beaker session when asking for ISession.
+        try:
+            import dolmen.beaker
+        except ImportError:
+            # no beaker installed, no test
+            return
+        setSite(self.getRootFolder()) # needed to start transaction
+        request = TestRequest()
+        session = ISession(request)
+        self.assertTrue(session.__module__.startswith('dolmen.beaker'))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_catalog.py	(revision 16045)
@@ -0,0 +1,186 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Tests for basic catalog components.
+"""
+import grok
+import unittest
+from grok import index
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, Attribute
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.catalog import FilteredQueryBase, FilteredCatalogQueryBase
+from waeup.kofa.interfaces import IFilteredQuery, IFilteredCatalogQuery
+
+class FilteredQueryBaseTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure, we correctly implement interface requirements
+        obj = FilteredQueryBase()
+        verifyClass(IFilteredQuery, FilteredQueryBase)
+        verifyObject(IFilteredQuery, obj)
+        return
+
+    def test_kw(self):
+        # keywords are set correctly on construction
+        q1 = FilteredQueryBase()
+        self.assertEqual(q1._kw, dict())
+        q2 = FilteredQueryBase(name='Bob')
+        self.assertEqual(q2._kw, dict(name='Bob'))
+        return
+
+    def test_kw_defaults(self):
+        # defaults are set initially but can be overridden.
+        class MyFilter(FilteredQueryBase):
+            defaults=dict(name='Bob')
+        q1 = MyFilter()
+        self.assertEqual(q1._kw, dict(name='Bob'))
+        q2 = MyFilter(name='Alice', age=29)
+        self.assertEqual(q2._kw, dict(name='Alice', age=29))
+        return
+
+    def test_query_not_implemented(self):
+        # we are warned when using `query()`
+        q = FilteredQueryBase()
+        self.assertRaises(NotImplementedError, q.query)
+        return
+
+
+# A sample catalog environment. Has to be grokked to work
+class Herd(grok.Container, grok.Application):
+    pass
+
+class IMammoth(Interface):
+    age = Attribute('Age')
+    name = Attribute('Name')
+
+    def message():
+        """Message for the world.
+        """
+
+class MammothCatalog(grok.Indexes):
+    grok.site(Herd)
+    grok.context(IMammoth)
+    grok.name('mammoths')
+
+    name = index.Field(attribute='name')
+    age = index.Field(attribute='age')
+
+class Mammoth(grok.Model):
+    grok.implements(IMammoth)
+
+    def __init__(self, name, age, message):
+        self.age = age
+        self.name = name
+        self._message = message
+
+    def message(self):
+        return self._message
+
+setup_done = False
+class FilteredCatalogQueryBaseTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        global setup_done
+        super(FilteredCatalogQueryBaseTests, self).setUp()
+        if not setup_done:
+            # grok this module to register indexes etc. defined above
+            # ...and do this only once
+            # XXX workaround because functional test cases do not support
+            #     classmethod setUp
+            grok.testing.grok(self.__module__)
+            setup_done = True
+        self.root = self.getRootFolder()
+        self.getRootFolder()['herd'] = Herd()
+        self.herd = self.root['herd']
+        self.populate_catalog()
+        self.catalog = getUtility(
+            ICatalog, context=self.herd, name='mammoths')
+        return
+
+    def populate_catalog(self):
+        setSite(self.herd)
+        self.herd['bob'] = Mammoth('Bob', 21, 'Hi there!')
+        self.herd['alice'] = Mammoth('Alice', 22, 'Hello!')
+        return
+
+    def test_iface(self):
+        # make sure, we correctly implement interface requirements
+        obj = FilteredCatalogQueryBase()
+        verifyClass(IFilteredCatalogQuery, FilteredCatalogQueryBase)
+        verifyObject(IFilteredCatalogQuery, obj)
+        return
+
+    def test_cat_name_respected(self):
+        # a set cat_name is respected when doing the query
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'mammoths' # set a registered catalog name
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Bob'])
+        return
+
+    def test_query_catalog(self):
+        # we can pass in a catalog instance
+        q = FilteredCatalogQueryBase(name='Bob')
+        result = q.query_catalog(catalog=self.catalog)
+        self.assertEqual([x.name for x in result], ['Bob'])
+        return
+
+    def test_query_catalog_value_ranges(self):
+        # we can ask for value ranges
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'))
+        result = q.query_catalog(catalog=self.catalog)
+        self.assertEqual([x.name for x in result], ['Bob', 'Alice'])
+        return
+
+    def test_query_value_ranges(self):
+        # we can ask for value ranges instead of single values
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'))
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Bob', 'Alice'])
+        return
+
+    def test_query_mixed_values(self):
+        # we can ask for different values at the same time
+        q = FilteredCatalogQueryBase(name=('Alice', 'Bob'), age=22)
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], ['Alice'])
+        return
+
+    def test_query_invalid_cat_name(self):
+        # with an invalid catalog name we get empty results
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'NOT-EXISTING'
+        result = q.query()
+        self.assertEqual([x.name for x in result], [])
+        return
+
+    def test_query_catalog_no_site(self):
+        # without a site with catalog we get empty results
+        setSite(None) # unset current site
+        q = FilteredCatalogQueryBase(name='Bob')
+        q.cat_name = 'mammoths'
+        result = q.query()
+        self.assertEqual([x.name for x in result], [])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_configuration.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_configuration.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_configuration.py	(revision 16045)
@@ -0,0 +1,71 @@
+## $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
+##
+"""Tests for configuration containers and related.
+"""
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.configuration import (
+    ConfigurationContainer, SessionConfiguration, SessionConfigurationFactory)
+from waeup.kofa.interfaces import(
+    IConfigurationContainer, ISessionConfiguration, ISessionConfigurationAdd)
+
+class ConfigurationTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ConfigurationTest, self).setUp()
+        self.confcontainer = ConfigurationContainer()
+        self.sessionconf = SessionConfiguration()
+        return
+
+    def tearDown(self):
+        super(ConfigurationTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IConfigurationContainer, ConfigurationContainer)
+        verify.verifyObject(IConfigurationContainer, self.confcontainer)
+        verify.verifyClass(ISessionConfiguration, SessionConfiguration)
+        verify.verifyObject(ISessionConfiguration, self.sessionconf)
+        verify.verifyClass(ISessionConfigurationAdd, SessionConfiguration)
+        verify.verifyObject(ISessionConfigurationAdd, self.sessionconf)
+
+class SessionConfigurationFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SessionConfigurationFactoryTest, self).setUp()
+        self.factory = SessionConfigurationFactory()
+
+    def tearDown(self):
+        super(SessionConfigurationFactoryTest, self).tearDown()
+
+    def test_interfaces(self):
+        verify.verifyClass(IFactory, SessionConfigurationFactory)
+        verify.verifyObject(IFactory, self.factory)
+
+    def test_factory(self):
+        obj = self.factory()
+        assert isinstance(obj, SessionConfiguration)
+
+    def test_getInterfaces(self):
+        implemented_by = self.factory.getInterfaces()
+        assert implemented_by.isOrExtends(ISessionConfiguration)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_datacenter.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_datacenter.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_datacenter.py	(revision 16045)
@@ -0,0 +1,286 @@
+# -*- coding: utf-8 -*-
+## Tests for datacenter
+##
+## XXX: Most tests for datacenter are still in doctest datacenter.txt
+##
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component import getUtility, getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.datacenter import DataCenter
+from waeup.kofa.interfaces import (
+    IDataCenter, IDataCenterConfig, IExportJobContainer)
+
+class DataCenterLogQueryTests(unittest.TestCase):
+    # Tests for querying logfiles via datacenter.
+
+    def setUp(self):
+        # create a temporary place to store files
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        self.logpath = os.path.join(self.storage, 'logs', 'myapp.log')
+        # register a datacenter config that provides the set up location
+        self.config = {'path': self.storage,}
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.config, IDataCenterConfig)
+        pass
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.config, IDataCenterConfig)
+        shutil.rmtree(self.workdir)
+        return
+
+    def fill_logfile(self, num=1):
+        # write 100 messages into logfile, with 50 containing 'Msg'
+        path = self.logpath
+        for m in range(num-1,-1,-1):
+            # write messages in order: lowest message in oldest file
+            fp = open(path, 'wb')
+            for n in range(50*m, 50*m+50):
+                fp.write('Msg %d\n' % (n + 1))
+                fp.write('Other Line %d\n' % (n + 1))
+            fp.write('A Message with Ümläüt')
+            fp.close()
+            path = self.logpath + '.%d' % m
+        return
+
+    def test_util_available(self):
+        # a self-test
+        config = getUtility(IDataCenterConfig)
+        self.assertTrue(config is not None)
+        return
+
+    def test_query_logfiles(self):
+        # We can find entries in logfiles
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Message 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Message'))
+        self.assertEqual(result, [u'Message 1\n'])
+        return
+
+    def test_query_logfiles_multi_logs(self):
+        # We can find entries in multiple logfiles (backups)
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 3\n')
+        open(self.logpath + '.2', 'wb').write('Msg 2\n')
+        open(self.logpath + '.10', 'wb').write('Msg 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # entry of logfile .10 comes after entry of logfile .2
+        self.assertEqual(result, [u'Msg 1\n', u'Msg 2\n', u'Msg 3\n'])
+        return
+
+    def test_query_logfiles_ignores_other_logs(self):
+        # We look only for the basename specified
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        open(self.logpath + '-not-a-real-log', 'wb').write('Msg 2\n')
+        open(self.logpath + '-not-a-real-log.1', 'wb').write('Msg 3\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # Msg 2 and 3 won't show up in results.
+        self.assertEqual(result, [u'Msg 1\n'])
+        return
+
+    def test_query_logfiles_not_existant(self):
+        # We make sure only existing logs are searched
+        datacenter = DataCenter()
+        open(self.logpath + '.1', 'wb').write('Msg 1\n')
+        result = list(datacenter.queryLogfiles('myapp.log', 'Msg'))
+        # works, although there is no myapp.log, only myapp.log.1
+        self.assertEqual(result, ['Msg 1\n'])
+        return
+
+    def test_query_logfiles_invalid_regexp(self):
+        # Invalid regular expressions give a ValueError
+        datacenter = DataCenter()
+        open(self.logpath, 'wb').write('Msg 1\n')
+        result = datacenter.queryLogfiles('myapp.log', '(a')
+        self.assertRaises(ValueError, list, result)
+        return
+
+    def test_query_logfiles_batching_limit(self):
+        # we can use `limit` for batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[-1], 'Msg 10\n')
+        return
+
+    def test_query_logfiles_batching_start(self):
+        # `start` is respected when batching
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25))
+        self.assertEqual(len(result), 25)
+        self.assertEqual(result[0], u'Msg 26\n')
+        return
+
+    def test_query_logfiles_batching_limit_and_start(self):
+        # we can use `start` and `limit` simultanously
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=25, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], u'Msg 26\n')
+        self.assertEqual(result[-1], u'Msg 35\n')
+        return
+
+    def test_query_logfiles_batching_edge_cases(self):
+        # we can find last matches if found num < limit.
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 5)
+        self.assertEqual(result[0], u'Msg 46\n')
+        self.assertEqual(result[-1], u'Msg 50\n')
+        return
+
+    def test_query_logfiles_batching_multiple_files(self):
+        # batching works also with multiple log files
+        datacenter = DataCenter()
+        self.fill_logfile(num=2)
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', 'Msg', start=45, limit=10))
+        self.assertEqual(len(result), 10)
+        self.assertEqual(result[0], u'Msg 46\n')
+        self.assertEqual(result[-1], u'Msg 55\n')
+        return
+
+    def test_query_logfiles_regex_match_inner(self):
+        # we also find lines that match at some inner part
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles('myapp.log', 'sg 1\n'))
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0], u'Msg 1\n')
+        return
+
+    def test_query_logfiles_umlauts(self):
+        # we return results as unicode decoded from utf-8
+        datacenter = DataCenter()
+        self.fill_logfile()
+        result = list(datacenter.queryLogfiles(
+            'myapp.log', u'Ümläüt'))
+        self.assertTrue(isinstance(result[0], unicode))
+        self.assertEqual(result, [u'A Message with Ümläüt'])
+        return
+
+class DataCenterTests(unittest.TestCase):
+    # General datacenter tests.
+
+    def setUp(self):
+        # create a temporary place to store files
+        self.workdir = tempfile.mkdtemp()
+        self.storage = os.path.join(self.workdir, 'storage')
+        os.mkdir(self.storage)
+        self.logpath = os.path.join(self.storage, 'logs', 'myapp.log')
+        # register a datacenter config that provides the set up location
+        self.config = {'path': self.storage,}
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.config, IDataCenterConfig)
+        pass
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.config, IDataCenterConfig)
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # we comply with interfaces
+        obj = DataCenter()
+        verifyClass(IDataCenter, DataCenter)
+        verifyClass(IExportJobContainer, DataCenter)
+        verifyObject(IDataCenter, obj)
+        verifyObject(IExportJobContainer, obj)
+        return
+
+    def test_get_log_files(self):
+        # We can get lists of logfiles available.
+        # By default, backups are skipped.
+        datacenter = DataCenter()
+        logpath2 = self.logpath + '.1'
+        logpath3 = self.logpath + '.2'
+        for path in self.logpath, logpath2, logpath3:
+            open(path, 'wb').write('some contents')
+        result = datacenter.getLogFiles()
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].name, os.path.basename(self.logpath))
+        return
+
+    def test_get_log_files_incl_backups(self):
+        # We can get lists of logfiles including backup logs.
+        datacenter = DataCenter()
+        logpath2 = self.logpath + '.1'
+        logpath3 = self.logpath + '.2'
+        for path in self.logpath, logpath2, logpath3:
+            open(path, 'wb').write('some contents')
+        result = datacenter.getLogFiles(exclude_backups=False)
+        self.assertEqual(len(result), 3)
+        names = [x.name for x in result]
+        expected = [os.path.basename(x) for x in [
+            self.logpath, logpath2, logpath3]]
+        self.assertEqual(names, expected)
+        return
+
+    def test_append_csv_file(self):
+        # we can append CSV files to others
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        open(csv_file1, 'wb').write('name,age\nBarney,28')
+        open(csv_file2, 'wb').write('name,age\nManfred,28')
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        self.assertEqual(result, 'age,name\r\n28,Barney\r\n28,Manfred\r\n')
+        # The source is deleted afterwards
+        self.assertEqual(os.path.exists(csv_file2), False)
+        return
+
+    def test_append_csv_file_no_dest(self):
+        # a non-existing dest CSV file will result in a simple move
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        # csv_file1 does not exist
+        open(csv_file2, 'wb').write('name,age\nManfred,28\n')
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # raw input, no CSV mangling
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        # The source is deleted afterwards
+        self.assertEqual(os.path.exists(csv_file2), False)
+        return
+
+    def test_append_csv_file_no_source(self):
+        # a non existing source file will mean no changes at all
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = os.path.join(self.workdir, 'bar.csv')
+        open(csv_file1, 'wb').write('name,age\nManfred,28\n')
+        # csv_file2 does not exist
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # csv_file1 is the same as before
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        return
+
+    def test_append_csv_file_same_src_and_dest(self):
+        # if both csv files are the same file, nothing will be changed
+        datacenter = DataCenter()
+        csv_file1 = os.path.join(self.workdir, 'foo.csv')
+        csv_file2 = csv_file1
+        open(csv_file1, 'wb').write('name,age\nManfred,28\n')
+        # csv_file2 does not exist
+        datacenter._appendCSVFile(csv_file2, csv_file1)
+        result = open(csv_file1, 'rb').read()
+        # csv_file1 is the same as before
+        self.assertEqual(result, 'name,age\nManfred,28\n')
+        self.assertEqual(os.path.exists(csv_file2), True)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_doctests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_doctests.py	(revision 16045)
@@ -0,0 +1,26 @@
+## $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
+##
+"""Register doctests for waeup.kofa.
+"""
+from waeup.kofa.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite([
+            'doctests/authentication.txt',
+            ])
+    return suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_fees.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_fees.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_fees.py	(revision 16045)
@@ -0,0 +1,136 @@
+# Tests for fee management.
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.fees import IFeeTable
+from waeup.kofa.fees import FeeTable, nested_list, nested_tuple
+
+SAMPLE_PARAMS = (('locals', 'non-locals'), ('art', 'science'),
+                 ('fac1', 'fac2'))
+
+SAMPLE_VALUES = (((10, 20),   # locals, art, fac1/fac2
+                  (11, 21)),  #         science, fac1/fac2
+                 ((12, 22),   # non-locals, art, fac1/fac2
+                  (13, 23)))  #         science, fac1/fac2
+
+class HelperTests(unittest.TestCase):
+
+    def test_nested_list(self):
+        # we can turn nested lists into nested tuples
+        self.assertEqual(
+            nested_list(()), [])
+        self.assertEqual(
+            nested_list((1,)), [1])
+        self.assertEqual(
+            nested_list(((1,2), (3,))), [[1,2],[3]])
+        self.assertEqual(
+            nested_list((((1,2),(3,4)),(5,6))), [[[1,2],[3,4]],[5,6]])
+        return
+
+    def test_nested_tuples(self):
+        # we can turn nested tuples into nested lists
+        self.assertEqual(
+            nested_tuple([]), ())
+        self.assertEqual(
+            nested_tuple([1,]), (1,))
+        self.assertEqual(
+            nested_tuple([[1,2], [3,]]), ((1,2),(3,)))
+        self.assertEqual(
+            nested_tuple([[[1,2],[3,4]],[5,6]]), (((1,2),(3,4)),(5,6)))
+        return
+
+class FeeTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure, we comply with interfaces
+        obj = FeeTable()
+        verifyClass(IFeeTable, FeeTable)
+        verifyObject(IFeeTable, obj)
+        return
+
+    def test_constructor(self):
+        # we can set values in constructor
+        fees = FeeTable(params=SAMPLE_PARAMS, values=SAMPLE_VALUES)
+        self.assertEqual(fees.values, SAMPLE_VALUES)
+        self.assertEqual(fees.params, SAMPLE_PARAMS)
+        return
+
+    def test_import_values(self):
+        # we can import a set of values (clearing old stuff)
+        fees = FeeTable()
+        fees.import_values(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertEqual(fees.params, SAMPLE_PARAMS)
+        self.assertEqual(fees.values, SAMPLE_VALUES)
+        return
+
+    def test_import_values_no_values(self):
+        # w/o values the table is emptied
+        fees = FeeTable()
+        fees.import_values()
+        self.assertEqual(fees.params, ())
+        self.assertEqual(fees.values, ())
+        return
+
+    def test_as_dict(self):
+        # we can get a fee table as dict
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result = fees.as_dict()
+        self.assertEqual(
+            result,
+            {
+                ('locals', 'art', 'fac1'): 10,
+                ('locals', 'science', 'fac1'): 11,
+                ('non-locals', 'art', 'fac1'): 12,
+                ('non-locals', 'science', 'fac1'): 13,
+                ('locals', 'art', 'fac2'): 20,
+                ('locals', 'science', 'fac2'): 21,
+                ('non-locals', 'art', 'fac2'): 22,
+                ('non-locals', 'science', 'fac2'): 23,
+                }
+            )
+
+    def test_get_fee(self):
+        # we can get values
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result1 = fees.get_fee(('locals', 'science', 'fac2'),)
+        result2 = fees.get_fee(('non-locals', 'art', 'fac1'),)
+        self.assertEqual(result1, 21)
+        self.assertEqual(result2, 12)
+        return
+
+    def test_get_fee_invalid_key(self):
+        # invalid param values will raise a key error
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertRaises(
+            KeyError, fees.get_fee, ('NONSENSE', 'science', 'fac2'))
+        self.assertRaises(
+            KeyError, fees.get_fee, ('locals', 'NONSENSE', 'fac2'))
+        self.assertRaises(
+            KeyError, fees.get_fee, ('locals', 'science', 'NONSENSE'))
+        # also the order of params matters
+        self.assertRaises( # 'science' and 'locals' is wrong order...
+            KeyError, fees.get_fee, ('science', 'locals', 'fac2'))
+        return
+
+    def test_set_fee(self):
+        # we can set values
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        result1 = fees.get_fee(('locals', 'science', 'fac2'),)
+        fees.set_fee(('locals', 'science', 'fac2'), 42)
+        result2 = fees.get_fee(('locals', 'science', 'fac2'),)
+        self.assertEqual(result1, 21)
+        self.assertEqual(result2, 42)
+        return
+
+    def test_set_fee_invalid(self):
+        # invalid param values will raise a key error
+        fees = FeeTable(SAMPLE_PARAMS, SAMPLE_VALUES)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('NONSENSE', 'science', 'fac2'), 12)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('locals', 'NONSENSE', 'fac2'), 12)
+        self.assertRaises(
+            KeyError, fees.set_fee, ('locals', 'science', 'NONSENSE'), 12)
+       # also the order of params matters
+        self.assertRaises( # 'science' and 'locals' is wrong order...
+            KeyError, fees.set_fee, ('science', 'locals', 'fac2'), 12)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_imagestorage.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_imagestorage.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_imagestorage.py	(revision 16045)
@@ -0,0 +1,400 @@
+## $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
+##
+import os
+import tempfile
+import shutil
+import unittest
+from StringIO import StringIO
+from hurry.file import HurryFile
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import (
+    getUtility, provideUtility, queryUtility, provideAdapter)
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.app import University
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.kofa.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class FileStoreNameChooserTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # we provide the interfaces we promise to do
+        obj = FileStoreNameChooser(None)
+        verifyClass(IFileStoreNameChooser, FileStoreNameChooser)
+        verifyObject(IFileStoreNameChooser, obj)
+        return
+
+    def test_accessible_as_adapter(self):
+        # we can get a file name chooser via adapter
+        chooser = IFileStoreNameChooser(object())
+        self.assertTrue(
+            isinstance(chooser, FileStoreNameChooser))
+        return
+
+    def test_check_name(self):
+        # default file name choosers accept any string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.checkName('Hi there!'), True)
+        self.assertEqual(chooser.checkName(None), False)
+        return
+
+    def test_choose_name(self):
+        # we get a simple string if we do not pass in a valid string
+        chooser = FileStoreNameChooser(object())
+        self.assertEqual(chooser.chooseName('myname'), 'myname')
+        self.assertEqual(chooser.chooseName(None), u'unknown_file')
+        return
+
+class ExtFileStoreTests(unittest.TestCase):
+    # Test external file store (non-functional mode)
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.root = None
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        if self.root is not None:
+            shutil.rmtree(self.root)
+        return
+
+    def test_iface(self):
+        obj = ExtFileStore(None)
+        verifyClass(IExtFileStore, ExtFileStore)
+        verifyObject(IExtFileStore, obj)
+        self.root = obj.root # for cleanup
+        return
+
+    def test_root_setup_wo_site(self):
+        # if no site is available we can use a temporary root
+        fs = ExtFileStore()
+        self.root = fs.root
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertTrue(os.path.exists(self.root))
+        return
+
+    def test_create_instance(self):
+        # we can create file stores with or without a root dir set
+        storage1 = ExtFileStore()
+        storage2 = ExtFileStore(root=self.workdir)
+        self.root = storage1.root
+        self.assertTrue(storage1.root is not None)
+        self.assertTrue(storage1.root != storage2.root)
+        self.assertEqual(storage2.root, self.workdir)
+        return
+
+    def test_create_file(self):
+        # We can store files
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        self.assertTrue('mysample' in os.listdir(storage.root))
+        self.assertEqual('mysample', image_file.data)
+        return
+
+    def test_create_file_w_ext(self):
+        # We can store files with filename extension
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample.txt', dummy_file)
+        self.assertTrue('mysample.txt' in os.listdir(storage.root))
+        self.assertEqual('mysample.txt', image_file.data)
+        return
+
+    def test_get_file(self):
+        # We can get files after having them stored
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample', dummy_file)
+        result = storage.getFile(image_file.data)
+        self.assertEqual(result.read(), 'sample file')
+        return
+
+    def test_get_file_w_ext(self):
+        # We can get files with filename extension after having them
+        # stored
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample file')
+        image_file = storage.createFile('mysample.txt', dummy_file)
+        result = storage.getFile('mysample')
+        self.assertEqual(result.read(), 'sample file')
+        return
+
+    def test_replace_file_w_new_ext(self):
+        # when we store a file with the same file_id but different
+        # filename extension, the old file will be deleted
+        storage = ExtFileStore(root=self.workdir)
+        dummy_file = StringIO('sample_file')
+        image_file = storage.createFile('mysample.jpg', dummy_file)
+        file1_path = storage.getFile('mysample').name
+        new_file = storage.createFile(
+            'mysample.png', StringIO('new file'))
+        file2 = storage.getFile('mysample')
+        self.assertEqual(file2.name[-12:], 'mysample.png')
+        self.assertEqual(file2.read(), 'new file')
+        # the old file was deleted
+        self.assertFalse(os.path.exists(file1_path))
+        return
+
+    def test_extract_marker(self):
+        # file stores support extracting markers from filenames
+        storage = ExtFileStore(root=self.workdir)
+        result1 = storage.extractMarker(None)
+        result2 = storage.extractMarker('')
+        result3 = storage.extractMarker('no-marker')
+        result4 = storage.extractMarker('no-marker.txt')
+        result5 = storage.extractMarker('__MARKER__foo.jpg')
+        result6 = storage.extractMarker('__MaRkEr__foo.jpg')
+        result7 = storage.extractMarker('__THE_MARKER__foo.jpg')
+        result8 = storage.extractMarker('__A_MARK__my__foo.jpg')
+
+        self.assertEqual(result1, ('', '', '', ''))
+        self.assertEqual(result2, ('', '', '', ''))
+        self.assertEqual(result3, ('', 'no-marker', 'no-marker', ''))
+        self.assertEqual(result4, ('', 'no-marker.txt', 'no-marker', '.txt'))
+        self.assertEqual(result5, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result6, ('marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result7, ('the_marker', 'foo.jpg', 'foo', '.jpg'))
+        self.assertEqual(result8, ('a_mark', 'my__foo.jpg', 'my__foo', '.jpg'))
+        return
+
+class DefaultFileStoreHandlerTests(unittest.TestCase):
+
+    def test_iface(self):
+        obj = DefaultFileStoreHandler()
+        verifyClass(IFileStoreHandler, DefaultFileStoreHandler)
+        verifyObject(IFileStoreHandler, obj)
+        return
+
+class CustomizedFileHandler(object):
+    def pathFromFileID(self, store, root, file_id):
+        return os.path.join(root, file_id[12:])
+
+    def createFile(self, store, root, filename, file_id, f):
+        ext = os.path.splitext(filename)[1]
+        path = self.pathFromFileID(store, root, file_id) + ext
+        return f, path, HurryFile(filename, file_id + ext)
+
+class CustomContext(object):
+    pass
+
+class CustomContextFileChooser(object):
+    def __init__(self, context):
+        self.context = context
+
+    def chooseName(self, name=None, attr=None):
+        # this name chooser returns different file ids depending on
+        # the `attr` parameter, a simple string.
+        if attr=='img':
+            return '__mymarker__mysample_img.jpg'
+        elif attr=='doc':
+            return '__mymarker__mysample_doc.doc'
+        return '__mymarker__mysample.txt'
+
+class FunctionalExtFileStoreTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalExtFileStoreTests, self).setUp()
+        self.workdir = tempfile.mkdtemp()
+        self.root = None
+        self.samplefile = os.path.join(self.workdir, 'sample')
+        self.otherfile = os.path.join(self.workdir, 'other')
+        open(self.samplefile, 'wb').write('Hi there!')
+        open(self.otherfile, 'wb').write('Hi from other!')
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+        self.getRootFolder()['app'] = University()
+        self.app = self.getRootFolder()['app']
+        self.app['datacenter'].setStoragePath(self.workdir)
+        # register a custom filename mangler
+        provideUtility(
+            CustomizedFileHandler(), IFileStoreHandler, name=u'mymarker')
+        # register a file chooser adapter for CustomContext
+        provideAdapter(
+            CustomContextFileChooser,
+            (CustomContext,), IFileStoreNameChooser)
+        return
+
+
+    def tearDown(self):
+        super(FunctionalExtFileStoreTests, self).tearDown()
+        self.fd.close()
+        self.fd2.close()
+        shutil.rmtree(self.workdir)
+        if self.root is not None and os.path.exists(self.root):
+            shutil.rmtree(self.root)
+        return
+
+    def test_root_setup_w_site(self):
+        # if a site is available we use it to determine the root dir
+        fs = ExtFileStore()
+        setSite(self.app)
+        self.root = fs.root
+        expected_root = os.path.join(
+            self.app['datacenter'].storage, 'media')
+        self.assertTrue(isinstance(self.root, basestring))
+        self.assertEqual(self.root, expected_root)
+        return
+
+    def test_get_utility(self):
+        # we can get an ExtFileStore by global utility lookup
+        fs1 = getUtility(IExtFileStore)
+        fs2 = getUtility(IExtFileStore)
+        self.assertTrue(isinstance(fs1, ExtFileStore))
+        self.assertTrue(fs1 is fs2)
+        return
+
+    def test_default_handler_create_file(self):
+        # we can use the default handler to store files
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile('sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, 'sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_default_handler_get_file(self):
+        # we can get files stored by the default handler
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile('sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('sample.txt')
+        result2 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        return
+
+    def test_customized_handler_create_file(self):
+        # we can use registered filename handlers
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile(
+            '__MYMARKER__sample.txt', StringIO('sample text'))
+        self.assertEqual(result.data, '__MYMARKER__sample.txt')
+        self.assertTrue('sample.txt' in os.listdir(fs.root))
+        return
+
+    def test_customized_handler_create_file_w_ext(self):
+        # when we create a file of img type, the filename ext is taken
+        # from input file.
+        fs = ExtFileStore(root=self.workdir)
+        result = fs.createFile(
+            '__MYMARKER__sample_img.png', StringIO('sample text'))
+        self.assertEqual(result.data, '__MYMARKER__sample_img.png')
+        self.assertTrue('sample_img.png' in os.listdir(fs.root))
+        return
+
+    def test_customized_handler_get_file(self):
+        # we consider registered filename handlers when asking for
+        # stored files.
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile('__MYMARKER__sample.txt', StringIO('sample text'))
+        result1 = fs.getFile('__MYMARKER__sample.txt')
+        result2 = fs.getFile('__MYMARKER__not-existent')
+        result3 = fs.getFile('not-existent')
+        self.assertEqual(result1.read(), 'sample text')
+        self.assertTrue(result2 is None)
+        self.assertTrue(result3 is None)
+        return
+
+    def test_get_file_by_context_w_attr(self):
+        # if we register a file name chooser, we can also get a file
+        # by context and attribute
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        # create three files for a single context, each with different
+        # content
+        fs = ExtFileStore(root=self.workdir)
+        fs.createFile(file_id1, StringIO('my sample 1'))
+        fs.createFile(file_id2, StringIO('my sample 2'))
+        fs.createFile(file_id3, StringIO('my sample 3'))
+        # now get back all files indicated by different `attr` markers
+        result1 = fs.getFileByContext(context)
+        result2 = fs.getFileByContext(context, attr='img')
+        result3 = fs.getFileByContext(context, attr='doc')
+        # each file has a different file id
+        self.assertEqual(file_id1, '__mymarker__mysample.txt')
+        self.assertEqual(file_id2, '__mymarker__mysample_img.jpg')
+        self.assertEqual(file_id3, '__mymarker__mysample_doc.doc')
+        # each file has different content
+        self.assertEqual(result1.read(), 'my sample 1')
+        self.assertEqual(result2.read(), 'my sample 2')
+        self.assertEqual(result3.read(), 'my sample 3')
+        return
+
+    def test_get_default_handler(self):
+        # we can get a default handler
+        result = queryUtility(IFileStoreHandler)
+        self.assertTrue(
+            isinstance(result, DefaultFileStoreHandler))
+        return
+
+    def test_get_default_file_retrieval(self):
+        # we get a file store when requesting a file retrieval
+        result = queryUtility(IFileRetrieval)
+        self.assertTrue(
+            isinstance(result, DefaultStorage))
+
+    def test_delete_file(self):
+        # we can remove stored files from storage
+        fs = ExtFileStore(root=self.workdir)
+        # First, we store a file in file store
+        fs.createFile('sample.txt', StringIO('sample text'))
+        # Then we delete it
+        fs.deleteFile('sample.txt')
+        # 'Deletion' means, next call to getFile should get None
+        result = fs.getFile('sample.txt')
+        self.assertTrue(result is None)
+        # Hm, okay we can also check, whether it was really deleted
+        self.assertTrue('sample.txt' not in os.listdir(fs.root))
+        return
+
+    def test_delete_file_by_context_w_attr(self):
+        # if we register a file name chooser, we can also delete a file
+        # by context and attribute
+        fs = ExtFileStore(root=self.workdir)
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        fs = ExtFileStore(root=self.workdir)
+        # create three files for a single context, each which
+        # different content
+        fs.createFile(file_id1, StringIO('my sample 1'))
+        fs.createFile(file_id2, StringIO('my sample 2'))
+        fs.createFile(file_id3, StringIO('my sample 3'))
+        # now delete first two of these files
+        fs.deleteFileByContext(context)
+        fs.deleteFileByContext(context, attr='img')
+        # Following getFile calls should give None for two of the
+        # files.
+        result1 = fs.getFileByContext(context)
+        result2 = fs.getFileByContext(context, attr='img')
+        result3 = fs.getFileByContext(context, attr='doc')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, None)
+        self.assertEqual(result3.read(), 'my sample 3')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_interfaces.py	(revision 16045)
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+## $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
+##
+
+# Tests for interfaces and included components.
+import grok
+import unittest
+from waeup.kofa.interfaces import DuplicationError, RoleSource, check_email
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+class RegExTests(unittest.TestCase):
+    # Let's test regular expressions here
+
+    def check(self, expression, string):
+        if expression.match(string) is not None:
+            return True
+        return False
+
+    def test_check_email_allowed(self):
+        # the RE check_email does moderate format checks, allowing
+        # also strange looking but valid email addresses
+        self.assertTrue(check_email('manfred@domain.com') is not None)
+        self.assertTrue(check_email('bob@localhost') is not None)
+        self.assertTrue(check_email('bob@h5.waeup.org') is not None)
+        self.assertTrue(check_email('bob@idn_with_ümlaut.com') is not None)
+        # also unicodes with umlauts work
+        self.assertTrue(check_email(u'bob@idn_with_ümlaut.com') is not None)
+        self.assertTrue(
+            check_email('bob_the-complex+mbox@my.place') is not None)
+        return
+
+    def test_check_email_forbidden(self):
+        # the RE check_email does moderate format checks, but doesn't
+        # accept everything.
+        # no domain at all
+        self.assertTrue(check_email('bob') is None)
+        self.assertTrue(check_email('bob@') is None)
+        # each domain part must contain at least something
+        self.assertTrue(check_email('bob@h5.waeup.') is None)
+        # whitespaces in addresses
+        self.assertTrue(check_email('spacey @domain.com') is None)
+        self.assertTrue(check_email('tabby\t@domain.com') is None)
+        # two dots next to each other in domain (empty domain part)
+        self.assertTrue(check_email('bob@some..domain') is None)
+        # commas are forbidden
+        self.assertTrue(check_email('bob@foo,alice@bar') is None)
+        return
+
+class DuplicationErrorTests(unittest.TestCase):
+
+    def test_is_exception(self):
+        exc = DuplicationError('Some message')
+        self.assertEqual(exc.msg, 'Some message')
+        self.assertEqual(exc.entries, [])
+
+    def test_as_string(self):
+        exc = DuplicationError('Some message')
+        self.assertEqual(exc.__str__(), "'Some message'")
+
+
+class RoleSourceTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(RoleSourceTests, self).setUp()
+        # Register a role
+        class SomeRole(grok.Role):
+            grok.name('waeup.testrole')
+            grok.title('RoleWith.DotName')
+            grok.permissions('waeup.Public')
+        # Register a role not visible to waeup portal as its name does
+        # not start with 'waeup.'
+        class NonKofaRole(grok.Role):
+            grok.name('nonwaeup.testrole')
+            grok.title('Role not suitable for waeup')
+            grok.permissions('waeup.Public')
+        grok.testing.grok_component('SomeRole', SomeRole)
+        grok.testing.grok_component('NonKofaRole', NonKofaRole)
+        return
+
+    def test_getValues(self):
+        source = RoleSource()
+        result = source.factory.getValues()
+        self.assertTrue(u'waeup.testrole' in result)
+        self.assertTrue(u'waeup.PortalManager' in result)
+        self.assertTrue(u'nonwaeup.testrole' not in result)
+
+    def test_getTitle(self):
+        source = RoleSource()
+        result = source.factory.getTitle(u'waeup.PortalManager')
+        self.assertEqual(result, 'Portal Manager')
+
+    def test_getTitleWithDot(self):
+        # If there is a dot in the role title, we get only the last part
+        source = RoleSource()
+        result = source.factory.getTitle(u'waeup.testrole')
+        self.assertEqual(result, 'DotName')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_maintenance.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_maintenance.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_maintenance.py	(revision 16045)
@@ -0,0 +1,130 @@
+import shutil
+import tempfile
+from grokcore.site.interfaces import IUtilityInstaller
+from zope import schema
+from zope.catalog.catalog import Catalog
+from zope.catalog.field import FieldIndex
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, implements
+from waeup.kofa.app import University
+from waeup.kofa.maintenance import update_catalog
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+
+class ISampleContent(Interface):
+
+    age = schema.Int(
+        title=u'Age')
+    name = schema.TextLine(
+        title=u'Name')
+
+class SampleContent(object):
+    implements(ISampleContent)
+
+    def __init__(self, age=None, name=None):
+        self.age = age
+        self.name = name
+        return
+
+    def __eq__(self, ob):
+        # make sample contents comparable
+        if not hasattr(ob, 'name') or not hasattr(ob, 'age'):
+            return False
+        return self.age == ob.age and self.name == ob.name
+
+class UpdateCatalogTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UpdateCatalogTests, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        super(UpdateCatalogTests, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def create_catalog(self):
+        # install catalog in site
+        self.catalog = Catalog()
+        install = getUtility(IUtilityInstaller)
+        install(self.app, self.catalog, ICatalog, 'my_catalog')
+        self.create_indexes()
+        self.catalog.clear() # make sure cat is empty
+        return
+
+    def create_indexes(self):
+        # create 'age' and 'name' field index in cat if they do not
+        # exist already.
+        for name in ('age', 'name'):
+            if name in self.catalog.keys():
+                continue
+            self.catalog[name] = FieldIndex(
+                field_name=name, interface=ISampleContent,
+                field_callable=False)
+        return
+
+    def add_sample_content(self):
+        # Add sample content in ZODB
+        manfred = SampleContent(42, 'Manfred')
+        self.app['manfred'] = manfred
+        self.manfred = self.app['manfred'] # this one has __parent__ etc.
+        return
+
+    def test_update_catalog_by_list(self):
+        ## We can catalog new objects passed in as list
+        # create object in ZODB before catalog exists
+        self.add_sample_content()
+        self.create_catalog()
+
+        # right now we can't find manfred
+        result1 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(len(result1), 0)
+
+        # update catalog. We have to feed 'located' objects, which is
+        # normally the case for objects from ZODB. In our case
+        # 'manfred' could not be catalogued while the located version
+        # 'manfred_located' (with a __parent__ and __name__ attribute)
+        # can.
+        update_catalog(self.app, 'my_catalog', [self.manfred])
+
+        # now we can find manfred in catalog
+        result2 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(result2[0], self.manfred)
+        return
+
+    def test_update_catalog_iterator(self):
+        ## We can catalog new objects via function
+        # create object in ZODB before catalog exists
+        self.add_sample_content()
+        self.create_catalog()
+
+        # right now we can't find manfred
+        result1 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(len(result1), 0)
+
+        # update catalog. We have to feed 'located' objects, which is
+        # normally the case for objects from ZODB. In our case
+        # 'manfred' could not be catalogued while the located version
+        # 'manfred_located' (with a __parent__ and __name__ attribute)
+        # can.
+        def func():
+            for x in [self.manfred]:
+                yield x
+        update_catalog(self.app, 'my_catalog', func=func)
+
+        # now we can find manfred in catalog
+        result2 = list(self.catalog.searchResults(age=(40,43)))
+        self.assertEqual(result2[0], self.manfred)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_objecthistory.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_objecthistory.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_objecthistory.py	(revision 16045)
@@ -0,0 +1,134 @@
+## $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 object history.
+"""
+import grok
+import shutil
+import tempfile
+from persistent.list import PersistentList
+from zope.interface.verify import verifyObject, verifyClass
+from zope.security.management import newInteraction, endInteraction
+from zope.security.testing import Principal, Participation
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IObjectHistory, IKofaObject
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.objecthistory import ObjectHistory
+
+class SampleObject(grok.Model):
+    grok.implements(IKofaObject)
+    pass
+
+class ObjectHistoryTests(FunctionalTestCase):
+    # Tests for helpers like get_access_code, disable_accesscode, ...
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ObjectHistoryTests, self).setUp()
+
+        # Prepopulate ZODB
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.obj = SampleObject()
+
+    def tearDown(self):
+        shutil.rmtree(self.dc_root)
+        super(ObjectHistoryTests, self).tearDown()
+        endInteraction() # Just in case, one is still lingering around
+
+    def test_iface(self):
+        # ObjectHistory class and instances provide the promised ifaces
+        hist = IObjectHistory(self.obj)
+        assert verifyObject(IObjectHistory, hist)
+        assert verifyClass(IObjectHistory, ObjectHistory)
+
+    def test_adapter(self):
+        # We can get a history by adapting to IObjectHistory
+        result = IObjectHistory(self.obj)
+        assert isinstance(result, ObjectHistory)
+
+    def test_add_messages(self):
+        # We can add a message
+        hist = IObjectHistory(self.obj)
+        assert hist.messages == []
+        hist.addMessage('blah')
+        assert 'blah' in ''.join(hist.messages)
+
+    def test_add_messages_timestamp_and_user(self):
+        # Messages added get a timestamp and the current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blah by system', result)
+
+    def test_add_messages_existing_principal(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blah by Bob', result)
+
+    def test_modify_messages(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        hist.modifyMessages('blah', 'blow')
+        result = ''.join(hist.messages)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blow by Bob', result)
+
+    def test_remove_message(self):
+        principal = Principal('bob')
+        principal.title = 'Bob'
+        newInteraction(Participation(principal)) # set current user
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('blah')
+        hist.addMessage('blow')
+        self.assertEqual(len(hist._getMessages()), 2)
+        result = ' '.join(hist.messages)
+        self.assertTrue('blah by Bob' in result)
+        self.assertTrue('blow by Bob' in result)
+        success, text = hist.removeMessage('xyz')
+        self.assertFalse(success)
+        self.assertEqual(text, 'Not a number')
+        success, text = hist.removeMessage(100)
+        self.assertFalse(success)
+        self.assertEqual(text, 'Number out of range')
+        success, text = hist.removeMessage(1)
+        self.assertTrue(success)
+        self.assertMatches('<YYYY-MM-DD hh:mm:ss> UTC - blow by Bob', text)
+        self.assertEqual(len(hist.messages), 1)
+        result = ' '.join(hist.messages)
+        self.assertFalse('blow by Bob' in result)
+
+    def test_messages(self):
+        # we get messages as a persistent list of strings
+        hist = IObjectHistory(self.obj)
+        hist.addMessage('foo')
+        hist.addMessage('bar')
+        assert isinstance(hist.messages, PersistentList)
+        assert 'foo' in hist.messages[0]
+        assert 'bar' in hist.messages[1]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_refereeentries.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_refereeentries.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_refereeentries.py	(revision 16045)
@@ -0,0 +1,111 @@
+# Tests for schoolgrades module.
+import unittest
+from zope.component import getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import ConstraintNotSatisfied, WrongType
+from waeup.kofa.interfaces import (
+    IRefereeEntry, IRefereeEntryField, IKofaUtils, NotAnEmailAddress)
+from waeup.kofa.refereeentries import RefereeEntry, RefereeEntryField
+from waeup.kofa.utils.utils import KofaUtils
+
+
+class RefereeEntryTests(unittest.TestCase):
+
+    def setUp(self):
+        self.valid_name = u'Otis Stone'
+        self.valid_email = 'otis@stones.de'
+        return
+
+    def tearDown(self):
+        return
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces
+        obj = RefereeEntry()
+        verifyObject(IRefereeEntry, obj)
+        verifyClass(IRefereeEntry, RefereeEntry)
+        return
+
+    def test_init(self):
+        # we can pass initial values
+        item1 = RefereeEntry()
+        item2 = RefereeEntry(self.valid_name, self.valid_email)
+        self.assertTrue(item1.name is None)
+        self.assertTrue(item1.email is None)
+        self.assertEqual(item2.name, self.valid_name)
+        self.assertEqual(item2.email, self.valid_email)
+        self.assertFalse(item1.email_sent)
+        self.assertFalse(item2.email_sent)
+        return
+
+    def test_illegal_value(self):
+        # we do not accept wrong values
+        item = RefereeEntry()
+        self.assertRaises(
+            WrongType, RefereeEntry, 'invalid', 'invalid')
+        self.assertRaises(
+            WrongType, RefereeEntry, 'invalid')
+        self.assertRaises(
+            WrongType, setattr, item, 'name', 99)
+        self.assertRaises(
+            NotAnEmailAddress, setattr, item, 'email', 'blah')
+        return
+
+    def test_to_string(self):
+        # the string representation is handy for export
+        item1 = RefereeEntry()
+        item2 = RefereeEntry(self.valid_name, self.valid_email)
+        self.assertEqual(item1.to_string(), u"(None, None, False)")
+        self.assertEqual(item2.to_string(), u"(u'%s', '%s', False)" % (
+            self.valid_name, self.valid_email))
+        return
+
+    def test_from_string(self):
+        # we can create new result entries based on strings
+        myinput = u"(u'%s','%s',True)" % (
+            self.valid_name, self.valid_email)
+        item1 = RefereeEntry.from_string(myinput)
+        item2 = RefereeEntry.from_string(u"(u'','',999)")
+        item3 = RefereeEntry.from_string(u"(None, None, False)")
+        self.assertEqual(item1.name, self.valid_name)
+        self.assertEqual(item1.email, self.valid_email)
+        self.assertTrue(item1.email_sent is True)
+        self.assertTrue(item2.name is None)
+        self.assertTrue(item2.email is None)
+        self.assertTrue(item2.email_sent is False)
+        self.assertTrue(item3.name is None)
+        self.assertTrue(item3.email is None)
+        self.assertTrue(item3.email_sent is False)
+        return
+
+    def test_eq(self):
+        # we can compare equality of RefereeEntry objects
+        item1 = RefereeEntry(self.valid_name, self.valid_email)
+        item2 = RefereeEntry(self.valid_name, self.valid_email)
+        item3 = RefereeEntry()
+        item4 = RefereeEntry()
+        assert item1 is not item2
+        assert item1 == item1
+        assert item1 == item2
+        assert item3 is not item4
+        assert item3 == item4
+        assert item1.__eq__(item2) is True
+        assert item1.__eq__(item3) is False
+
+    def test_ne(self):
+        # we can also tell, which ResultEntries are _not_ equal
+        item1 = RefereeEntry(self.valid_name, self.valid_email)
+        item2 = RefereeEntry()
+        assert item1 != item2
+        assert item1.__ne__(item2) is True
+        assert item1.__ne__(item1) is False
+
+
+class RefereeEntryFieldTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = RefereeEntryField()
+        verifyObject(IRefereeEntryField, obj)
+        verifyClass(IRefereeEntryField, RefereeEntryField)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_reports.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_reports.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_reports.py	(revision 16045)
@@ -0,0 +1,445 @@
+import datetime
+import grok
+import logging
+import pytz
+import unittest
+from cStringIO import StringIO
+from grok.interfaces import IContainer
+from zc.async.interfaces import IJob
+from zc.async.testing import wait_for_result
+from zope.component import getGlobalSiteManager, getUtility
+from zope.component.hooks import setSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.interfaces import IJobManager, IKofaPluggable
+from waeup.kofa.reports import (
+    IReport, IReportGenerator, IReportJob, IReportJobContainer,
+    IReportsContainer,)
+from waeup.kofa.reports import (
+    Report, ReportGenerator, get_generators, report_job, AsyncReportJob,
+    ReportJobContainer, ReportsContainer, ReportsContainerPlugin)
+from waeup.kofa.testing import FakeJob, FakeJobManager, FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+
+class FakeReportGenerator(ReportGenerator):
+    # fake report generator for tests.
+
+    def __init__(self, name=None, perm_create=None, perm_view=None):
+        self.title = 'Report 1'
+        self.perm_create = perm_create
+        self.perm_view = perm_view
+        return
+
+    def __eq__(self, obj):
+        if getattr(obj, 'title', None) is None:
+            return False
+        return self.title == obj.title
+
+    def generate(self, site, args=[], kw={}):
+        result = Report()
+        result.args = args
+        result.kw = kw
+        return result
+
+class ReportTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill the promised contracts
+        obj = Report()
+        verifyClass(IReport, Report)
+        verifyObject(IReport, obj)
+        return
+
+    def test_creation_dt(self):
+        # a report sets a datetime timestamp when created
+        report = Report()
+        self.assertTrue(hasattr(report, 'creation_dt'))
+        self.assertTrue(isinstance(report.creation_dt, datetime.datetime))
+        # the datetime is set with UTC timezone info
+        self.assertEqual(report.creation_dt.tzinfo, pytz.utc)
+        return
+
+    def test_args(self):
+        # a report stores the args and kw used for generation
+        report1 = Report()
+        report2 = Report(args=[1, 2], kwargs=dict(a=1, b=2))
+        self.assertEqual(report1.args, [])
+        self.assertEqual(report1.kwargs, dict())
+        self.assertEqual(report2.args, [1, 2])
+        self.assertEqual(report2.kwargs, dict(a=1, b=2))
+        return
+
+    def test_create_pdf(self):
+        # trying to create a pdf results in an error
+        report = Report()
+        self.assertRaises(NotImplementedError, report.create_pdf, 'JOB_ID')
+        return
+
+class ReportGeneratorTest(unittest.TestCase):
+    def setUp(self):
+        grok.testing.grok('waeup.kofa.reports') # register utils
+
+    def test_iface(self):
+        # make sure we fullfill the promised contracts
+        obj = ReportGenerator()
+        verifyClass(IReportGenerator, ReportGenerator)
+        verifyObject(IReportGenerator, obj)
+        return
+
+    def test_generate(self):
+        # the base report generator delivers reports
+        obj = ReportGenerator()
+        result = obj.generate(None)
+        self.assertTrue(IReport.providedBy(result))
+        return
+
+class GeneratorRegistrar(object):
+    # Mix-in providing report generator registrations
+    def setUp(self):
+        self.registered = []
+
+    def tearDown(self):
+        # unregister any previously registered report types
+        gsm = getGlobalSiteManager()
+        for report, name in self.registered:
+            gsm.unregisterUtility(report, IReportGenerator, name=name)
+        return
+
+    def register_generator(self, name, perm_create=None, perm_view=None):
+        # helper to register report generators as utils
+        gsm = getGlobalSiteManager()
+        generator = FakeReportGenerator(name)
+        gsm.registerUtility(generator, provided=IReportGenerator, name=name)
+        self.registered.append((generator, name),)
+        return generator
+
+class HelpersTests(GeneratorRegistrar, unittest.TestCase):
+    # Tests for helper functions
+
+    def setUp(self):
+        grok.testing.grok('waeup.kofa.reports') # register utils
+        super(HelpersTests, self).setUp()
+        return
+
+    def test_get_generators_none(self):
+        # we get no generators if none was registered
+        result = list(get_generators())
+        self.assertEqual(result, [])
+        return
+
+    def test_get_generators_simple(self):
+        # we get a single generator if one is registered
+        self.register_generator('report1')
+        result = list(get_generators())
+        self.assertEqual(
+            result, [('report1', FakeReportGenerator('report1'))])
+        return
+
+    def test_get_generators_multiple(self):
+        # we also get multiple generators if available
+        self.register_generator('report1')
+        self.register_generator('report2')
+        result = list(get_generators())
+        self.assertEqual(
+            result,
+            [(u'report1', FakeReportGenerator('report1')),
+             (u'report2', FakeReportGenerator('report2'))])
+        return
+
+
+class ReportJobTests(FunctionalAsyncTestCase, GeneratorRegistrar):
+    # Test asynchronous report functionality (simple cases)
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ReportJobTests, self).setUp()
+        GeneratorRegistrar.setUp(self)
+        self.root_folder = self.getRootFolder()
+
+    def test_report_job_func(self):
+        # the report_job func really creates reports...
+        self.register_generator('report1')
+        report = report_job(None, 'report1')
+        self.assertTrue(IReport.providedBy(report))
+        self.assertTrue(isinstance(report, Report))
+
+    def test_report_job_interfaces(self):
+        # the AsyncReportJob implements promised interfaces correctly...
+        job = AsyncReportJob(None, None)
+        verifyClass(IJob, AsyncReportJob)
+        verifyObject(IJob, job)
+        verifyClass(IReportJob, AsyncReportJob)
+        verifyObject(IReportJob, job)
+        return
+
+    def test_finished(self):
+        # AsyncReportJobs signal with a bool whether they`re  finished
+        job = AsyncReportJob(self.root_folder, None)
+        setSite(self.root_folder)
+        self.assertEqual(job.finished, False)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        self.assertEqual(job.finished, True)
+        return
+
+    def test_failed_true(self):
+        # We can test whether a job failed
+        job = AsyncReportJob(self.root_folder, None) # no report generator
+        setSite(self.root_folder)
+        # while a job is not finished, `failed` is ``None``
+        self.assertTrue(job.failed is None)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        # the finished job failed
+        self.assertEqual(job.failed, True)
+        return
+
+    def test_failed_false(self):
+        # We can test whether a job failed
+        self.register_generator('report1')
+        job = AsyncReportJob(self.root_folder, 'report1')
+        setSite(self.root_folder)
+        # while a job is not finished, `failed` is ``None``
+        self.assertTrue(job.failed is None)
+        manager = getUtility(IJobManager)
+        manager.put(job)
+        wait_for_result(job)
+        # the finished job failed
+        self.assertEqual(job.failed, False)
+        return
+
+class FakeJobWithResult(FakeJob):
+
+    def __init__(self, args=[], kw={}):
+        self.result = Report()
+        self.result.args = args
+        self.result.kw = kw
+        return
+
+class ReportJobContainerTests(unittest.TestCase):
+    # Test ReportJobContainer
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.generator = FakeReportGenerator('report1')
+        self.job_manager = FakeJobManager()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.generator, IReportGenerator, name='report1')
+        self.gsm.registerUtility(
+            self.job_manager, IJobManager)
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(
+            self.generator, IReportGenerator, name='report1')
+        self.gsm.unregisterUtility(self.job_manager, IJobManager)
+
+    def test_report_job_interfaces(self):
+        # the ExportJobContainer implements promised interfaces correctly...
+        container = ReportJobContainer()
+        verifyClass(IReportJobContainer, ReportJobContainer)
+        verifyObject(IReportJobContainer, container)
+        return
+
+    def test_start_report_job(self):
+        # we can start jobs
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        result = self.job_manager._jobs.values()[0]
+        self.assertTrue(IJob.providedBy(result))
+        self.assertEqual(
+            container.running_report_jobs,
+            [('1', 'report3', 'bob')]
+            )
+        return
+
+    def test_get_running_report_jobs_all(self):
+        # we can get report jobs of all users
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'alice')
+        result = container.get_running_report_jobs()
+        self.assertEqual(
+            result,
+            [('1', 'report3', 'bob'),
+             ('2', 'report3', 'alice')]
+            )
+        return
+
+    def test_get_running_report_jobs_user(self):
+        # we can get the report jobs running for a certain user
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'alice')
+        result1 = container.get_running_report_jobs(user_id='alice')
+        result2 = container.get_running_report_jobs(user_id='foo')
+        self.assertEqual(
+            result1, [('2', 'report3', 'alice')])
+        self.assertEqual(
+            result2, [])
+        return
+
+    def test_get_running_report_jobs_only_if_exist(self):
+        # we get only jobs that are accessible through the job manager...
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        container.start_report_job('report3', 'bob')
+        self.assertTrue(
+            ('2', 'report3', 'bob') in container.running_report_jobs)
+        # we remove the second entry from job manager
+        del self.job_manager._jobs['2']
+        result = container.get_running_report_jobs(user_id='bob')
+        self.assertEqual(
+            result, [('1', 'report3', 'bob')])
+        self.assertTrue(
+            ('2', 'report3', 'bob') not in container.running_report_jobs)
+        return
+
+    def test_get_report_job_status(self):
+        # we can get the stati of jobs...
+        container = ReportJobContainer()
+        container.start_report_job('report1', 'alice')
+        container.start_report_job('report1', 'bob')
+        container.start_report_job('report1', 'bob')
+        result = container.get_report_jobs_status(user_id='bob')
+        # we'll get the raw value, a translation and the title of the
+        # exporter
+        self.assertEqual(
+            result,
+            [('new', u'new', u'Report 1'),
+             ('completed', u'completed', u'Report 1')]
+            )
+        return
+
+    def test_delete_report_entry(self):
+        # we can remove report entries in local lists and the job
+        # manager as well...
+        container = ReportJobContainer()
+        container.start_report_job('report3', 'bob')
+        entry = container.running_report_jobs[0]
+        container.delete_report_entry(entry)
+        # both, running_report_jobs list and job manager are empty now
+        self.assertEqual(
+            container.running_report_jobs, [])
+        self.assertEqual(
+            self.job_manager._jobs, {})
+        return
+
+    def test_report_entry_from_job_id(self):
+        # we can get an report entry for a job_id if the id exists
+        container = ReportJobContainer()
+        entry = ('4', 'report3', 'bob')
+        container.running_report_jobs = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        result1 = container.report_entry_from_job_id(None)
+        result2 = container.report_entry_from_job_id('4')
+        result3 = container.report_entry_from_job_id('23')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, ('4', 'report3', 'bob'))
+        self.assertEqual(result3, None)
+        return
+
+
+class ReportsContainerTests(unittest.TestCase):
+    # Tests for ReportsContainer
+
+    def test_iface(self):
+        # ReportsContainers really provide the promised interfaces
+        obj = ReportsContainer()
+        verifyClass(IReportsContainer, ReportsContainer)
+        verifyClass(IContainer, ReportsContainer)
+        verifyObject(IReportsContainer, obj)
+        verifyObject(IContainer, obj)
+        return
+
+class ReportsContainerPluginTests(unittest.TestCase):
+    # Tests for ReportsContainerPlugin
+
+    def create_logger(self):
+        # create a logger suitable for local tests.
+        test_logger = logging.getLogger('waeup.kofa.reports.testlogger')
+        log = StringIO()
+        handler = logging.StreamHandler(log)
+        handler.setLevel(logging.DEBUG)
+        test_logger.addHandler(handler)
+        test_logger.setLevel(logging.DEBUG)
+        self.logger = test_logger
+        self.log = log
+        self.handler = handler
+        return self.logger
+
+    def remove_logger(self):
+        del self.handler
+        del self.logger
+        del self.log
+        pass
+
+    def get_log(self):
+        self.log.seek(0)
+        return self.log.read()
+
+    def setUp(self):
+        self.create_logger()
+        return
+
+    def tearDown(self):
+        self.remove_logger()
+        return
+
+    def test_iface(self):
+        # make sure we fullfill the promised interfaces
+        obj = ReportsContainerPlugin()
+        verifyClass(IKofaPluggable, ReportsContainerPlugin)
+        verifyObject(IKofaPluggable, obj)
+        return
+
+    def test_get_as_utility(self):
+        # make sure we can get the plugin as utility
+        grok.testing.grok('waeup.kofa.reports')
+        util = getUtility(IKofaPluggable, name='reports')
+        self.assertTrue(util is not None)
+        return
+
+    def test_update_no_container(self):
+        # we can update an existing site
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        plugin.update(fake_site, 'app', self.logger)
+        log = self.get_log()
+        self.assertEqual(
+            log, 'Added reports container for site "app"\n')
+        self.assertTrue('reports' in fake_site.keys())
+        self.assertTrue(IReportsContainer.providedBy(fake_site['reports']))
+        return
+
+    def test_update_uptodate_site(self):
+        # we leave already existing reports containers in place
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        fake_site['reports'] = ReportsContainer()
+        plugin.update(fake_site, 'app', self.logger)
+        log = self.get_log()
+        self.assertEqual(log, '') # no log message
+        return
+
+    def test_setup_new_site(self):
+        # if we setup a site, we always install a fresh reports container
+        fake_site = grok.Container()
+        plugin = ReportsContainerPlugin()
+        plugin.setup(fake_site, 'app', self.logger)
+        log1 = self.get_log()
+        result1 = fake_site.get('reports', None)
+        plugin.setup(fake_site, 'app', self.logger) # replace old container
+        log2 = self.get_log()
+        result2 = fake_site.get('reports', None)
+        self.assertTrue(result1 is not result2)
+        self.assertEqual(log1,
+                         'Added reports container for site "app"\n')
+        self.assertEqual(log2,
+                         'Added reports container for site "app"\n'
+                         'Removed reports container for site "app"\n'
+                         'Added reports container for site "app"\n')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_schoolgrades.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_schoolgrades.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_schoolgrades.py	(revision 16045)
@@ -0,0 +1,109 @@
+# Tests for schoolgrades module.
+import unittest
+from zope.component import getGlobalSiteManager
+from zope.interface.verify import verifyObject, verifyClass
+from zope.schema.interfaces import ConstraintNotSatisfied
+from waeup.kofa.interfaces import IResultEntry, IResultEntryField, IKofaUtils
+from waeup.kofa.schoolgrades import ResultEntry, ResultEntryField
+from waeup.kofa.utils.utils import KofaUtils
+
+
+class ResultEntryTests(unittest.TestCase):
+
+    def setUp(self):
+        self.utils = KofaUtils()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(self.utils, IKofaUtils)
+        self.valid_subj = self.utils.EXAM_SUBJECTS_DICT.keys()[0]
+        self.valid_grade = self.utils.EXAM_GRADES[0][0]
+        return
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.utils)
+        return
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntry()
+        verifyObject(IResultEntry, obj)
+        verifyClass(IResultEntry, ResultEntry)
+        return
+
+    def test_init(self):
+        # we can pass initial values
+        item1 = ResultEntry()
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        self.assertTrue(item1.subject is None)
+        self.assertTrue(item1.grade is None)
+        self.assertEqual(item2.subject, self.valid_subj)
+        self.assertEqual(item2.grade, self.valid_grade)
+        return
+
+    def test_illegal_value(self):
+        # we do not accept values not stored in KofaUtils
+        item = ResultEntry()
+        self.assertRaises(
+            ConstraintNotSatisfied, ResultEntry, 'invalid', 'invalid')
+        self.assertRaises(
+            ConstraintNotSatisfied, ResultEntry, 'invalid')
+        self.assertRaises(
+            ConstraintNotSatisfied, setattr, item, 'subject', 'blah')
+        self.assertRaises(
+            ConstraintNotSatisfied, setattr, item, 'grade', 'blah')
+        return
+
+    def test_to_string(self):
+        # the string representation is handy for export
+        item1 = ResultEntry()
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        self.assertEqual(item1.to_string(), u"(None, None)")
+        self.assertEqual(item2.to_string(), u"('%s', '%s')" % (
+            self.valid_subj, self.valid_grade))
+        return
+
+    def test_from_string(self):
+        # we can create new result entries based on strings
+        myinput = u"(u'%s',u'%s')" % (
+            self.valid_subj, self.valid_grade)
+        item1 = ResultEntry.from_string(myinput)
+        item2 = ResultEntry.from_string(u"(u'',u'')")
+        item3 = ResultEntry.from_string(u"(None, None)")
+        self.assertEqual(item1.subject, self.valid_subj)
+        self.assertEqual(item1.grade, self.valid_grade)
+        self.assertTrue(item2.subject is None)
+        self.assertTrue(item2.grade is None)
+        self.assertTrue(item3.subject is None)
+        self.assertTrue(item3.grade is None)
+        return
+
+    def test_eq(self):
+        # we can compare equality of ResultEntry objects
+        item1 = ResultEntry(self.valid_subj, self.valid_grade)
+        item2 = ResultEntry(self.valid_subj, self.valid_grade)
+        item3 = ResultEntry()
+        item4 = ResultEntry()
+        assert item1 is not item2
+        assert item1 == item1
+        assert item1 == item2
+        assert item3 is not item4
+        assert item3 == item4
+        assert item1.__eq__(item2) is True
+        assert item1.__eq__(item3) is False
+
+    def test_ne(self):
+        # we can also tell, which ResultEntries are _not_ equal
+        item1 = ResultEntry(self.valid_subj, self.valid_grade)
+        item2 = ResultEntry()
+        assert item1 != item2
+        assert item1.__ne__(item2) is True
+        assert item1.__ne__(item1) is False
+
+
+class ResultEntryFieldTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # make sure we implement the promised interfaces.
+        obj = ResultEntryField()
+        verifyObject(IResultEntryField, obj)
+        verifyClass(IResultEntryField, ResultEntryField)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_smtp.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_smtp.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_smtp.py	(revision 16045)
@@ -0,0 +1,414 @@
+# -*- coding: utf-8 -*-
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+# Tests for email-related components
+import base64
+import logging
+import tempfile
+import shutil
+import unittest
+from StringIO import StringIO
+from zope.component import getUtility
+from zope.component.hooks import setSite
+from zope.sendmail.interfaces import IMailDelivery
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import IMailService
+from waeup.kofa.smtp import (
+    encode_header_item, encode_address, encode_body, FakeSMTPDelivery,
+    send_mail)
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+#
+# SMTP test config
+#
+
+# Also run tests that send mail to external servers?
+#   If you enable this, please make sure the external smtp hosts and receivers
+#   do exist really and are not bothered by being spammed by a test programme.
+EXTERNAL_MAIL_TESTS = False
+
+# Maybe existing receiver of externally sent mail. If this mail account
+# exists really, it might receive mail from the tests!
+EXTERNAL_MAIL_RECEIVER = 'no-reply@waeup.org'
+
+# Names of mail deliveries to use when external mail tests are enabled.
+#   See local mail.zcml for names of available deliveries.
+EXTERNAL_DIRECT_DELIVERY = 'Direct SMTP on localhost'
+EXTERNAL_QUEUED_DELIVERY = 'Queued SMTP on localhost'
+
+#
+# end of SMTP test config
+#
+
+
+def external_mail_test(func):
+    if not EXTERNAL_MAIL_TESTS:
+        myself = __file__
+        if myself.endswith('.pyc'):
+            myself = myself[:-1]
+        print "WARNING: external mail tests are skipped!"
+        print "WARNING: edit %s to enable them." % myself
+        return
+    return func
+
+
+class HelperTests(unittest.TestCase):
+
+    def test_encode_header_item(self):
+        # encoding header items works with strings and unicodes, as
+        # long as we pass in utf-8 encoded stuff.
+        result1 = encode_header_item(u'Plain Name'.encode('utf-8'))
+        result2 = encode_header_item(u'Name with umläut'.encode('utf-8'))
+        result3 = encode_header_item(u'Plain Name')
+        result4 = encode_header_item(u'Name with umläut')
+        result5 = encode_header_item(None)
+        self.assertEqual(result1, u'Plain Name')
+        self.assertEqual(result2, u'=?iso-8859-1?q?Name_with_uml=E4ut?=')
+        self.assertEqual(result3, u'Plain Name')
+        self.assertEqual(result4, u'=?iso-8859-1?q?Name_with_uml=E4ut?=')
+        self.assertTrue(result5 is None)
+        return
+
+    def test_encode_address(self):
+        # we can correctly encode address parts
+        result1 = encode_address('foo@bar.baz')
+        result2 = encode_address(u'foo@bar.baz', 'The Foo')
+        result3 = encode_address('foo@bar.baz', u'The Foo')
+        result4 = encode_address('foo@bar.baz', u'With Umläut')
+        self.assertEqual(result1, 'foo@bar.baz')
+        self.assertEqual(result2, 'The Foo <foo@bar.baz>')
+        self.assertEqual(result3, 'The Foo <foo@bar.baz>')
+        self.assertEqual(result4,
+                         '=?iso-8859-1?q?With_Uml=E4ut?= <foo@bar.baz>')
+        return
+
+    def test_encode_body(self):
+        result1 = encode_body(u'Simple Text')
+        self.assertEqual(
+            str(result1).split('\n')[1:],
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="us-ascii"',
+             'Content-Transfer-Encoding: 7bit',
+             '',
+             'Simple Text'])
+        return
+
+    def test_encode_body_latin1_string(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in ascii but available in latin1
+        text = u'Simple text with Ümläut'.encode('utf-8')
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="iso-8859-1"',
+             'Content-Transfer-Encoding: quoted-printable',
+             '',
+             'Simple text with =DCml=E4ut'])
+        return
+
+    def test_encode_body_latin1_unicode(self):
+        # unicode strings are transferred correctly when they contain
+        # chars unrepresentable in ascii but available in latin1
+        text = u'Simple text with Ümläut'
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="iso-8859-1"',
+             'Content-Transfer-Encoding: quoted-printable',
+             '',
+             'Simple text with =DCml=E4ut'])
+        return
+
+    def test_encode_body_utf8_string(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in ascii but available in latin1
+        text = u"Simple text with ü and capital pi: " + unichr(0x3a0)
+        text = text.encode('utf-8')  # turn unicode into byte stream
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="utf-8"',
+             'Content-Transfer-Encoding: base64',
+             '',
+             'U2ltcGxlIHRleHQgd2l0aCDDvCBhbmQgY2FwaXRhbCBwaTogzqA=', ''])
+        self.assertEqual(
+            base64.b64decode(result1[-2]).decode('utf-8'),
+            u"Simple text with ü and capital pi: " + unichr(0x3a0))
+        return
+
+    def test_encode_body_utf8_unicode(self):
+        # utf-8 encoded byte streams are transferred correctly when
+        # they contain chars unrepresentable in latin1
+        text = u"Simple text with ü and capital pi: " + unichr(0x3a0)
+        result1 = encode_body(text)
+        result1 = str(result1).split('\n')[1:]
+        self.assertEqual(
+            result1,
+            ['MIME-Version: 1.0',
+             'Content-Type: text/plain; charset="utf-8"',
+             'Content-Transfer-Encoding: base64',
+             '',
+             'U2ltcGxlIHRleHQgd2l0aCDDvCBhbmQgY2FwaXRhbCBwaTogzqA=', ''])
+        self.assertEqual(
+            base64.b64decode(result1[-2]).decode('utf-8'),
+            u"Simple text with ü and capital pi: " + unichr(0x3a0))
+        return
+
+
+class FunctionalMailerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(FunctionalMailerTests, self).setUp()
+        self.setup_logging()
+        return
+
+    def tearDown(self):
+        super(FunctionalMailerTests, self).tearDown()
+        self.teardown_logging()
+        return
+
+    def setup_logging(self):
+        # setup a log-handler that catches all fake mailer output
+        self.stream = StringIO()
+        handler = logging.StreamHandler(self.stream)
+        logger = logging.getLogger('test.smtp')
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return
+
+    def get_fake_smtp_output(self):
+        # get output generated by fake mailer
+        self.stream.flush()
+        self.stream.seek(0)
+        return self.stream.read()
+
+    def teardown_logging(self):
+        # remove the log handler for fake mailer output
+        logger = logging.getLogger('test.smtp')
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            logger.removeHandler(handler)
+        return
+
+    def test_get_fake_mailer(self):
+        # we can get the fake mailer if we want
+        mailer = getUtility(IMailDelivery, name='No email service')
+        self.assertTrue(isinstance(mailer, FakeSMTPDelivery))
+        return
+
+    def test_get_default_mailer(self):
+        # we can get a default mailer if we want
+        mailer = getUtility(IMailService)
+        self.assertTrue(isinstance(mailer(), FakeSMTPDelivery))
+        return
+
+    def test_send_mail(self):
+        # we can really send mail.
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail.')
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org to '
+             u'recpt@example.com, sender@example.com:',
+             u'Message:',
+             u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="us-ascii"',
+             u'msg: Content-Transfer-Encoding: 7bit',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail.',
+             u'']
+            )
+        return
+
+    def test_send_mail_with_cc(self):
+        # with cc=True the message will be CCed to the from address
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail.',
+            cc=True)
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org to '
+             u'recpt@example.com, sender@example.com:',
+             u'Message:',
+             u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="us-ascii"',
+             u'msg: Content-Transfer-Encoding: 7bit',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail.',
+             u'']
+            )
+        return
+
+    def test_send_mail_utf8_strings(self):
+        # we can send mail with utf-8 encoded strings as input
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail with ümläut.'.encode('utf-8'))
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org '
+             u'to recpt@example.com, sender@example.com:',
+             u'Message:', u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="iso-8859-1"',
+             u'msg: Content-Transfer-Encoding: quoted-printable',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail with =FCml=E4ut.',
+             u'']
+            )
+        return
+
+    def test_send_mail_utf8_unicode(self):
+        # we can send mail with utf-8 encoded unicode as input
+        mail_id = send_mail(
+            u'A sender', u'sender@example.com',
+            u'A recipient', u'recpt@example.com',
+            u'A subject',
+            u'This is a test mail with ümläut.')
+        self.assertEqual(mail_id, 'fake-message-id@example.com')
+        self.assertEqual(
+            self.get_fake_smtp_output().split('\n'),
+            [u'Sending email from no-reply@waeup.org '
+             u'to recpt@example.com, sender@example.com:',
+             u'Message:', u'msg: MIME-Version: 1.0',
+             u'msg: Content-Type: text/plain; charset="iso-8859-1"',
+             u'msg: Content-Transfer-Encoding: quoted-printable',
+             u'msg: From: A sender <no-reply@waeup.org>',
+             u'msg: To: A recipient <recpt@example.com>',
+             # u'msg: Cc: A sender <sender@example.com>',
+             u'msg: Reply-To: A sender <sender@example.com>',
+             u'msg: Subject: A subject',
+             u'msg: ',
+             u'msg: This is a test mail with =FCml=E4ut.',
+             u'']
+            )
+        return
+
+
+class ExternalMailerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ExternalMailerTests, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        return
+
+    def tearDown(self):
+        super(ExternalMailerTests, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_config_default_mailer(self):
+        # The default mailer set in config is 'no mailer'.
+        self.assertEqual(
+            getattr(self.app.get('configuration', None), 'smtp_mailer'),
+            u'No email service')
+        return
+
+    @external_mail_test
+    def test_send_direct_mail(self):
+        # send mail using direct mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_DIRECT_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver', EXTERNAL_MAIL_RECEIVER,
+            'Test Mail from WAeUP Kofa',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
+
+    @external_mail_test
+    def test_send_direct_mails(self):
+        # send several mails using direct mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_DIRECT_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver',
+            '%s, %s' % (EXTERNAL_MAIL_RECEIVER, EXTERNAL_MAIL_RECEIVER),
+            'Test Mail from WAeUP Kofa',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
+
+    @external_mail_test
+    def test_send_queued_mail(self):
+        # send mail using queued mail delivery
+        self.app['configuration'].smtp_mailer = EXTERNAL_QUEUED_DELIVERY
+        setSite(self.app)
+        result = send_mail(
+            'test program', 'no-reply@waeup.org',
+            'test mail receiver', EXTERNAL_MAIL_RECEIVER,
+            'Test Mail from WAeUP Kofa',
+            'Hi from test mailer!\n\nRegards,\nTest Programme\n'
+            )
+        import transaction
+        transaction.commit()  # The mail is really sent when transactions is
+                              # committed
+        self.assertTrue('@' in result)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_sourcefactory.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_sourcefactory.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_sourcefactory.py	(revision 16045)
@@ -0,0 +1,61 @@
+## Tests for waeup.kofa.sourcefactory.
+import unittest
+from waeup.kofa.sourcefactory import SmartBasicContextualSourceFactory
+
+class MyContext(object):
+    # a sample context with min and max values
+    minimum = 6
+    maximum = 8
+
+class MyContextualSource(SmartBasicContextualSourceFactory):
+    # a contextual sample source containing all values from context
+    def __init__(self, defaults):
+        super(MyContextualSource, self).__init__()
+        self.defaults = defaults
+
+    def getValues(self, context):
+        # This tuple could become quite large. Bad for containment checks.
+        return self.defaults + tuple(
+            range(context.minimum, context.maximum + 1))
+
+class MySmartContextualSource(MyContextualSource):
+    # a smart contextual sample source that can check a value for
+    # containment fast, even if there are zillions of values in the
+    # source.
+    def contains(self, context, value):
+        if value in self.defaults:
+            return True
+        # The context max is excluded. So we know that this method was
+        # called.
+        if value >= context.minimum and value < context.maximum:
+            return True
+        return False
+
+class SmartBasicContextualSourceFactoryTests(unittest.TestCase):
+
+    def test_default_get_values(self):
+        # smart bcsfs behave like regular ones when asked for values.
+        default_values = (4, 5, 6)
+        source = MyContextualSource(default_values)(MyContext())
+        self.assertEqual(list(source), [4, 5, 6, 6, 7, 8])
+        return
+
+    def test_default_contains(self):
+        # smart bcsfs can behave like regular ones when checking
+        # containment.  The default method is very expensive as it
+        # scans each single value of the source.
+        default_values = (4, 5, 6)
+        source = MyContextualSource(default_values)(MyContext())
+        self.assertEqual(6 in source, True)
+        self.assertEqual(8 in source, True)
+        self.assertEqual(10 in source, False)
+        return
+
+    def test_smart_contains(self):
+        # smart bcfss support smarter containment checks.
+        default_values = (4, 5, 6)
+        source = MySmartContextualSource(default_values)(MyContext())
+        self.assertEqual(6 in source, True)
+        self.assertEqual(8 in source, False) # the special contains()
+        self.assertEqual(10 in source, False)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_startup.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_startup.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_startup.py	(revision 16045)
@@ -0,0 +1,76 @@
+# Tests for local startup functions.
+import os
+import shutil
+import tempfile
+import unittest
+from zope.app.wsgi import WSGIPublisherApplication
+from waeup.kofa.startup import env_app_factory, env_debug_app_factory
+
+ZOPE_CONF_TEMPL = '''
+site-definition %s
+
+<zodb>
+  <mappingstorage />
+</zodb>
+
+<eventlog>
+  <logfile>
+    path STDOUT
+  </logfile>
+</eventlog>
+'''
+
+class StartUpTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.site_zcml = os.path.join(self.workdir, 'site.zcml')
+        open(self.site_zcml, 'w').write('<configure />')
+        self.zope_conf = os.path.join(self.workdir, 'zope.conf')
+        open(self.zope_conf, 'wb').write(ZOPE_CONF_TEMPL % self.site_zcml)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        for name in ('TEST_FOO', 'TEST_BAR'):
+            if name in os.environ:
+                del os.environ[name]
+        return
+
+    def test_env_app_factory(self):
+        # We can create plain WSGI apps
+        factory = env_app_factory({'zope_conf': self.zope_conf})
+        self.assertTrue(
+            isinstance(factory, WSGIPublisherApplication))
+        return
+
+    def test_env_app_factory_single_env_var(self):
+        # We can create WSGI apps with a single env var set
+        factory = env_app_factory({'zope_conf': self.zope_conf,
+                                   'env_vars': 'TEST_FOO value1'})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        return
+
+    def test_env_app_factory_multiple_env_vars(self):
+        # We can create WSGI apps with multiple env vars set
+        env_vars = 'TEST_FOO  value1   \n  TEST_BAR  value2'
+        factory = env_app_factory({'zope_conf': self.zope_conf,
+                                   'env_vars': env_vars})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        self.assertEqual(os.environ.get('TEST_BAR', None), 'value2')
+        return
+
+    def test_env_debug_app_factory(self):
+        # We can create debugger WSGI apps
+        factory = env_debug_app_factory({'zope_conf': self.zope_conf})
+        self.assertTrue(
+            isinstance(factory, WSGIPublisherApplication))
+        return
+        pass
+
+    def test_env_debug_app_factory_single_env_var(self):
+        # We can create debugger WSGI apps with a single env var set
+        factory = env_debug_app_factory({'zope_conf': self.zope_conf,
+                                         'env_vars': 'TEST_FOO value1'})
+        self.assertEqual(os.environ.get('TEST_FOO', None), 'value1')
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_waeup.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_waeup.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/test_waeup.py	(revision 16045)
@@ -0,0 +1,37 @@
+## $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 setup for the waeup.kofa package.
+"""
+import grok
+import re
+import zope.component.eventtesting
+from zope.testing import renormalizing
+from waeup.kofa.testing import FunctionalLayer
+
+def setUpZope(test):
+    zope.component.eventtesting.setUp(test)
+
+checker = renormalizing.RENormalizing([
+        (re.compile('[\d]{10}'), '<10-DIGITS>'),
+        ])
+
+# Register all tests in the waeup.kofa package
+test_suite = grok.testing.register_all_tests(
+    'waeup.kofa', checker=checker, usetup=setUpZope,
+    layer=FunctionalLayer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/util.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/util.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/tests/util.py	(revision 16045)
@@ -0,0 +1,43 @@
+## $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
+##
+import random
+
+def get_randstring(length):
+    result = ''
+    for i in range(1, length):
+        result += random.choice('abcdefghijklmnopqrstuvwxyz')
+    return result
+
+def get_randname():
+    result = ''
+    n1 = get_randstring(random.randint(4, 15))
+    n2 = get_randstring(random.randint(4, 15))
+    n3 = get_randstring(random.randint(4, 15))
+    #n1[0] = n1[0].upper()
+    #n2[0] = n2[0].upper()
+    #n3[0] = n3[0].upper()
+    return '"%s %s %s"' % (n1, n2, n3) 
+
+def get_namelist(num):
+    return [get_randname() for x in range(0, num)]
+
+random.seed()
+fd = open('namelist.txt', 'w')
+for name in get_namelist(70000):
+    fd.write(name + '\n')
+fd.close()
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/__init__.py	(revision 16045)
@@ -0,0 +1,12 @@
+# Make this a package.
+from waeup.kofa.university.course import Course
+from waeup.kofa.university.coursescontainer import CoursesContainer
+from waeup.kofa.university.certificate import Certificate
+from waeup.kofa.university.certificatescontainer import CertificatesContainer
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.university.facultiescontainer import FacultiesContainer
+__all__ = (
+    'Course', 'CoursesContainer',
+    'Certificate', 'CertificatesContainer',
+    'Faculty', 'FacultiesContainer', 'Department')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/batching.py	(revision 16045)
@@ -0,0 +1,430 @@
+## $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
+##
+"""Batch processing components for academics objects.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+
+Here we define the processors for academics specific objects like
+faculties, departments and the like.
+"""
+import grok
+from zope.interface import Interface
+from zope.component import queryUtility
+from zope.schema import getFields
+from zope.catalog.interfaces import ICatalog
+from zope.event import notify
+from zope.securitypolicy.interfaces import (
+    IPrincipalRoleManager, IPrincipalRoleMap)
+from waeup.kofa.authentication import LocalRoleSetEvent
+from waeup.kofa.interfaces import (
+    IBatchProcessor, IGNORE_MARKER, DELETION_MARKER, FatalCSVError)
+from waeup.kofa.university.interfaces import (
+    IFacultiesContainer, IFaculty, ICourse, IDepartment, ICertificate,
+    ICertificateCourse)
+from waeup.kofa.university import (
+    Faculty, Department, Course, Certificate)
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.interfaces import MessageFactory as _
+
+class FacultyProcessor(BatchProcessor):
+    """The Faculty Processor processes faculties in the `faculties` container.
+    The `FacultyProcessor` class also serves as a baseclass for all other 
+    batch processors in the academic section.
+
+    The processor makes some efforts to set local roles.
+    If new roles are provided, the `updateEntry` method first removes
+    all existing roles and then sets the new roles as given in the import
+    file. That means the entire set of local roles is replaced.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'facultyprocessor'
+    grok.name(util_name)
+
+    name = _('Faculty Processor')
+    iface = IFaculty
+    allowed_roles = Faculty.local_roles
+
+    location_fields = ['code',]
+    factory_name = 'waeup.Faculty'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'faculties' in site.keys()
+
+    @property
+    def available_fields(self):
+        fields = getFields(self.iface)
+        return sorted(list(set(
+            self.location_fields + fields.keys() + ['local_roles']
+            )))
+
+    def entryExists(self, row, site):
+        return row['code'] in site['faculties'].keys()
+
+    def getParent(self, row, site):
+        return site['faculties']
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['code'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addFaculty(obj)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        pass
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+        """
+        items_changed = ''
+
+        if 'local_roles' in row and row['local_roles'] not in (
+            None, IGNORE_MARKER):
+            role_manager = IPrincipalRoleManager(obj)
+            role_map = IPrincipalRoleMap(obj)
+            # Remove all existing local roles.
+            for local_role, user_name, setting in \
+                role_map.getPrincipalsAndRoles():
+                role_manager.unsetRoleForPrincipal(local_role, user_name)
+                notify(LocalRoleSetEvent(
+                        obj, local_role, user_name, granted=False))
+            # Add new local roles.
+            if row['local_roles'] != DELETION_MARKER:
+                local_roles = eval(row['local_roles'])
+                for rolemap in local_roles:
+                    user = rolemap['user_name']
+                    local_role = rolemap['local_role']
+                    role_manager.assignRoleToPrincipal(local_role, user)
+                    notify(LocalRoleSetEvent(
+                        obj, local_role, user, granted=True))
+                    items_changed += ('%s=%s, '
+                        % ('local_roles', '%s|%s' % (user,local_role)))
+            row.pop('local_roles')
+
+        # apply other values...
+        items_changed += super(FacultyProcessor, self).updateEntry(
+            obj, row, site, filename)
+
+        # Log actions...
+        location_field = self.location_fields[0]
+        grok.getSite().logger.info('%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return items_changed
+
+    def checkConversion(self, row, mode='create'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict =  super(
+            FacultyProcessor, self).checkConversion(row, mode=mode)
+        if 'local_roles' in row:
+            if row['local_roles'] in (None, DELETION_MARKER, IGNORE_MARKER):
+                return errs, inv_errs, conv_dict
+            try:
+                local_roles = eval(row['local_roles'])
+            except:
+                errs.append(('local_roles','Error'))
+                return errs, inv_errs, conv_dict
+            if not isinstance(local_roles, list):
+                errs.append(('local_roles','no list'))
+                return errs, inv_errs, conv_dict
+            for rolemap in local_roles:
+                if not isinstance(rolemap, dict):
+                    errs.append(('local_roles','no dicts'))
+                    return errs, inv_errs, conv_dict
+                if not 'user_name' in rolemap.keys() or not \
+                    'local_role' in rolemap.keys():
+                    errs.append((
+                        'local_roles','user_name or local_role missing'))
+                    return errs, inv_errs, conv_dict
+                local_role = rolemap['local_role']
+                if not local_role in self.allowed_roles:
+                    errs.append(('local_roles','%s not allowed' % local_role))
+                    return errs, inv_errs, conv_dict
+                user = rolemap['user_name']
+                users = grok.getSite()['users']
+                if not user in users.keys():
+                    errs.append(('local_roles','%s does not exist' % user))
+                    return errs, inv_errs, conv_dict
+        return errs, inv_errs, conv_dict
+
+class DepartmentProcessor(FacultyProcessor):
+    """The Department Processor works in the same way as the Faculty
+    Processor. Since department codes are not necessarily unique, it needs the
+    `faculty_code` to create and update objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'departmentprocessor'
+    grok.name(util_name)
+
+    name = _('Department Processor')
+    iface = IDepartment
+    allowed_roles = Department.local_roles
+
+    location_fields = ['code', 'faculty_code']
+    factory_name = 'waeup.Department'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        if not 'faculties' in site.keys():
+            return False
+        return row['faculty_code'] in site['faculties']
+
+    def entryExists(self, row, site):
+        if not self.parentsExist(row, site):
+            return False
+        parent = self.getParent(row, site)
+        return row['code'] in parent.keys()
+
+    def getParent(self, row, site):
+        return site['faculties'][row['faculty_code']]
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get(row['code'])
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addDepartment(obj)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        return
+
+class CertificateProcessor(FacultyProcessor):
+    """The Certificate Processor gets the parent object (the
+    `certificates` attribute of the department container) in two ways.
+    If both faculty and department codes are provided, `getPartents` uses
+    these to locate the certificate. If department code or
+    faculty code are missing, it use the certificates catalog to find the
+    certificate.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificateprocessor'
+    grok.name(util_name)
+
+    name = _('Certificate Processor')
+    iface = ICertificate
+    allowed_roles = Certificate.local_roles
+
+    location_fields = ['code']
+    factory_name = 'waeup.Certificate'
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        fields = getFields(self.iface)
+        return sorted(list(set(
+            ['faculty_code','department_code'] + fields.keys()
+            + ['local_roles'])))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        super(CertificateProcessor, self).checkHeaders(headerfields, mode)
+        if mode == 'create':
+            if not 'faculty_code' in headerfields \
+                or not 'department_code' in headerfields :
+                raise FatalCSVError(
+                    "Need at least columns faculty_code and department_code")
+        return True
+
+    def parentsExist(self, row, site):
+        return self.getParent(row,site) is not None
+
+    def entryExists(self, row, site):
+        parent = self.getParent(row, site)
+        if parent is not None:
+            return row['code'] in parent.keys()
+        return False
+
+    def getParent(self, row, site):
+        if not 'faculties' in site.keys():
+            return None
+        # If both faculty and department codes are provided, use
+        # these to get parent.
+        if row.get('faculty_code',None) not in (None, IGNORE_MARKER) and \
+            row.get('department_code',None) not in (None, IGNORE_MARKER):
+            if not row['faculty_code'] in site['faculties'].keys():
+                return None
+            faculty = site['faculties'][row['faculty_code']]
+            if not row['department_code'] in faculty.keys():
+                return None
+            dept = faculty[row['department_code']]
+            return dept.certificates
+        # If department code or faculty code is missing,
+        # use catalog to get parent. 
+        cat = queryUtility(ICatalog, name='certificates_catalog')
+        results = list(
+            cat.searchResults(code=(row['code'], row['code'])))
+        if results:
+            return results[0].__parent__
+        return None
+
+    def getEntry(self, row, site):
+        parent = self.getParent(row, site)
+        if parent is not None:
+            return parent.get(row['code'])
+        return None
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addCertificate(obj)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        return
+
+class CourseProcessor(CertificateProcessor):
+    """The Course Processor works exactly in the same way as the
+    Certificate Processor. It uses the courses catalog instead of the
+    certificates catalog.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseprocessor'
+    grok.name(util_name)
+
+    name = _('Course Processor')
+    iface = ICourse
+    allowed_roles = Course.local_roles
+
+    location_fields = ['code']
+    factory_name = 'waeup.Course'
+
+    mode = None
+
+    def getParent(self, row, site):
+        if not 'faculties' in site.keys():
+            return None
+        # If both faculty and department codes are provided, use
+        # these to get parent.
+        if row.get('faculty_code',None) not in (None, IGNORE_MARKER) and \
+            row.get('department_code',None) not in (None, IGNORE_MARKER):
+            if not row['faculty_code'] in site['faculties'].keys():
+                return None
+            faculty = site['faculties'][row['faculty_code']]
+            if not row['department_code'] in faculty.keys():
+                return None
+            dept = faculty[row['department_code']]
+            return dept.courses
+        # If department code or faculty code is missing,
+        # use catalog to get parent. Makes only sense in update mode but
+        # does also work in create mode.
+        cat = queryUtility(ICatalog, name='courses_catalog')
+        results = list(
+            cat.searchResults(code=(row['code'], row['code'])))
+        if results:
+            return results[0].__parent__
+        return None
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        parent.addCourse(obj)
+        return
+
+class CertificateCourseProcessor(FacultyProcessor):
+    """The Certificate Course Processor needs more location fields.
+    Certificate courses are stored inside the certificate container.
+    Thus, `faculty_code`, `department_code` and the
+    `certificate_code` are necessary to find the parent container.
+    It furthermore needs the `course` and the `level` field to locate
+    existing objects as they are part of the object id (code).
+    Consequently, `course` and `level` can't be updated. If the level
+    has changed, the object must be replaced (removed and re-created),
+    for instance by processing two import files.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificatecourseprocessor'
+    grok.name(util_name)
+
+    name = _('CertificateCourse Processor')
+    iface = ICertificateCourse
+
+    location_fields = ['certificate_code', 'course', 'level', 'faculty_code',
+                       'department_code',]
+    factory_name = 'waeup.CertificateCourse'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        if not 'faculties' in site.keys():
+            return False
+        if not row['faculty_code'] in site['faculties'].keys():
+            return False
+        faculty = site['faculties'][row['faculty_code']]
+        if not row['department_code'] in faculty.keys():
+            return False
+        dept = faculty[row['department_code']]
+        return row['certificate_code'] in dept.certificates.keys()
+
+    def entryExists(self, row, site):
+        if not self.parentsExist(row, site):
+            return False
+        parent = self.getParent(row, site)
+        code = "%s_%s" % (row['course'].code, row['level'])
+        return code in parent.keys()
+
+    def getParent(self, row, site):
+        dept = site['faculties'][row['faculty_code']][row['department_code']]
+        return dept.certificates[row['certificate_code']]
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        parent = self.getParent(row, site)
+        return parent.get("%s_%s" % (row['course'].code, row['level']))
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        # mandatory is not a required and might be missing or can be
+        # the ignore-marker.
+        mandatory = row.get('mandatory', None)
+        if mandatory in (None, IGNORE_MARKER):
+            parent.addCertCourse(row['course'], row['level'])
+            return
+        parent.addCertCourse(row['course'], row['level'], mandatory)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        parent.delCertCourses(row['course'].code, row['level'])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/catalog.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/catalog.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/catalog.py	(revision 16045)
@@ -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
+##
+"""Catalog and searching components for academics stuff.
+"""
+import grok
+from grok import index
+from hurry.query import Eq, Text
+from hurry.query.query import Query
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.interfaces import ComponentLookupError
+from zope.index.text.parsetree import ParseError
+from zope.intid import IIntIds
+#from waeup.kofa.catalog import QueryResultItem
+from waeup.kofa.interfaces import IUniversity, IQueryResultItem
+from waeup.kofa.university.interfaces import (
+    ICourse, ICertificateCourse, IDepartment,
+    ICertificate,
+    )
+
+class CourseIndexes(grok.Indexes):
+    """This catalog is needed for building sources.
+    """
+    grok.site(IUniversity)
+    grok.name('courses_catalog')
+    grok.context(ICourse)
+
+    code = index.Field(attribute='code')
+    title = index.Text(attribute='title')
+
+class CertificatesIndexes(grok.Indexes):
+    """This catalog is needed for building sources.
+    """
+    grok.site(IUniversity)
+    grok.name('certificates_catalog')
+    grok.context(ICertificate)
+
+    code = index.Field(attribute='code')
+    application_category = index.Field(attribute='application_category')
+    title = index.Text(attribute='title')
+
+class CertificateCoursesIndexes(grok.Indexes):
+    """This catalog is needed for automatic removal of certificate courses
+    and later for selection of course tickets in the students section.
+    """
+    grok.site(IUniversity)
+    grok.name('certcourses_catalog')
+    grok.context(ICertificateCourse)
+
+    course_code = index.Field(attribute='getCourseCode')
+    level = index.Field(attribute='level')
+
+@grok.subscribe(ICourse, grok.IObjectAddedEvent)
+def handle_course_added(obj, event):
+    """Index an add course with the local catalog.
+
+    Courses are not indexed automatically, as they are not a
+    dictionary subitem of the accompanied site object
+    (`IUniversity`). I.e. one cannot get them by asking for
+    ``app['FACCODE']['DEPTCODE']['COURSECODE']`` but one has to ask for
+    ``app.faculties['FACCODE']['DEPTCODE'].courses['COURSECODE']``.
+
+    Once, a course is indexed we can leave the further handling to
+    the default component architechture. At least removals will
+    be handled correctly then (and the course unindexed).
+    """
+    try:
+        cat = getUtility(ICatalog, name='courses_catalog')
+    except ComponentLookupError:
+        # catalog not available. This might happen during tests.
+        return
+    intids = getUtility(IIntIds)
+    index = cat['code']
+    index.index_doc(intids.getId(obj), obj)
+
+@grok.subscribe(ICourse, grok.IObjectAddedEvent)
+def handle_certificate_added(obj, event):
+    """Index an added certificate with the local catalog.
+
+    See handleCourseAdd.
+    """
+    try:
+      cat = getUtility(ICatalog, name='certificates_catalog')
+    except ComponentLookupError:
+      # catalog not available. This might happen during tests.
+      return
+    intids = getUtility(IIntIds)
+    index = cat['code']
+    index.index_doc(intids.getId(obj), obj)
+
+@grok.subscribe(ICertificateCourse, grok.IObjectAddedEvent)
+def handlecertificatecourse_added(obj, event):
+    """Index an added certificatecourse with the local catalog.
+
+    See handleCourseAdd.
+    """
+    try:
+      cat = getUtility(ICatalog, name='certcourses_catalog')
+    except ComponentLookupError:
+      # catalog not available. This might happen during tests.
+      return
+    intids = getUtility(IIntIds)
+    index = cat['course_code']
+    index.index_doc(intids.getId(obj), obj)
+
+@grok.subscribe(IDepartment, grok.IObjectRemovedEvent)
+def handle_department_removed(obj, event):
+    """Clear courses and certificates when a department is killed.
+    """
+    # We cannot use the 'clear()' method of respective subcontainers
+    # (courses, certificates), because that would not trigger
+    # IObjectRemoved events.
+    for subobj_name in ['courses', 'certificates']:
+        key_list = list(getattr(obj, subobj_name, {}).keys())
+        for key in key_list:
+            del getattr(obj, subobj_name)[key]
+    return
+
+class CoursesQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.title = context.title
+        self.code = context.code
+        self.dep = context.__parent__.__parent__.code
+        self.fac = context.__parent__.__parent__.__parent__.code
+        self.type = 'Course'
+
+class CertificatesQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.title = context.title
+        self.code = context.code
+        self.dep = context.__parent__.__parent__.code
+        self.fac = context.__parent__.__parent__.__parent__.code
+        self.type = 'Certificate'
+
+class CertificateCoursesQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.title = context.course.title
+        self.code = context.getCourseCode
+        self.dep = context.__parent__.__parent__.__parent__.code
+        self.fac = context.__parent__.__parent__.__parent__.__parent__.code
+        self.type = 'Certificate Course'
+
+def search(query=None, view=None):
+    if not query:
+        view.flash('Empty search string.')
+        return
+
+    hitlist = []
+    try:
+        results = Query().searchResults(
+            Eq(('courses_catalog', 'code'), query))
+        for result in results:
+            hitlist.append(CoursesQueryResultItem(result, view=view))
+        results = Query().searchResults(
+            Text(('courses_catalog', 'title'), query))
+        for result in results:
+            hitlist.append(CoursesQueryResultItem(result, view=view))
+        results = Query().searchResults(
+            Eq(('certificates_catalog', 'code'), query))
+        for result in results:
+            hitlist.append(CertificatesQueryResultItem(result, view=view))
+        results = Query().searchResults(
+            Text(('certificates_catalog', 'title'), query))
+        for result in results:
+            hitlist.append(CertificatesQueryResultItem(result, view=view))
+        results = Query().searchResults(
+            Eq(('certcourses_catalog', 'course_code'), query))
+        for result in results:
+            hitlist.append(CertificateCoursesQueryResultItem(result, view=view))
+    except ParseError:
+        view.flash('Search string not allowed.')
+        return
+    return hitlist
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificate.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificate.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificate.py	(revision 16045)
@@ -0,0 +1,290 @@
+## $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
+##
+"""Kofa certificates and certificate courses
+"""
+import grok
+import zope.location.location
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.intid.interfaces import IIntIds
+from zope.schema import getFields
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.university.interfaces import (
+    ICertificate, ICertificateCourse)
+from waeup.kofa.university.vocabularies import course_levels
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+
+class VirtualCertificateExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for certificates.
+    """
+
+class Certificate(grok.Container):
+    """A certificate.
+    """
+    grok.implements(ICertificate)
+
+    local_roles = [
+        'waeup.local.CourseAdviser100',
+        'waeup.local.CourseAdviser200',
+        'waeup.local.CourseAdviser300',
+        'waeup.local.CourseAdviser400',
+        'waeup.local.CourseAdviser500',
+        'waeup.local.CourseAdviser600',
+        'waeup.local.CourseAdviser700',
+        'waeup.local.CourseAdviser800',
+        'waeup.local.DepartmentOfficer',
+        'waeup.local.ClearanceOfficer',
+        ]
+
+    def __init__(self, code=u'NA', title=u'Unnamed Certificate',
+                 study_mode=None, start_level=None,
+                 end_level=None, application_category=None,
+                 school_fee_1=None, school_fee_2=None,
+                 school_fee_3=None, school_fee_4=None,
+                 ratio=None, degree=None,
+                 custom_textline_1=None, custom_textline_2=None,
+                 custom_float_1=None, custom_float_2=None):
+        super(Certificate, self).__init__()
+        self.code = code
+        self.title = title
+        self.study_mode = study_mode
+        self.start_level = start_level
+        self.end_level = end_level
+        self.application_category = application_category
+        self.school_fee_1 = school_fee_1
+        self.school_fee_2 = school_fee_2
+        self.school_fee_3 = school_fee_3
+        self.school_fee_4 = school_fee_4
+        self.ratio = ratio
+        self.degree = degree
+        self.custom_textline_1 = custom_textline_1
+        self.custom_textline_2 = custom_textline_2
+        self.custom_float_1 = custom_float_1
+        self.custom_float_2 = custom_float_2
+
+    def traverse(self, name):
+        """Deliver appropriate containers.
+        """
+        if name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualCertificateExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+    @property
+    def longtitle(self):
+        return "%s (%s)" % (self.title,self.code)
+
+    def addCertCourse(self, course, level=100,
+                      mandatory=True, course_category=None):
+        """Add a certificate course.
+        """
+        code = "%s_%s" % (course.code, level)
+        self[code] = CertificateCourse(course, level, mandatory, course_category)
+        self[code].__parent__ = self
+        self[code].__name__ = code
+        self._p_changed = True
+
+    def delCertCourses(self, code, level=None):
+        """Delete certificate courses.
+
+        We might have more than one certificate course for a course.
+        If level is not provided all certificate courses referring
+        to the same course will be deleted.
+        """
+        keys = list(self.keys()) # create list copy
+        for key in keys:
+            if self[key].getCourseCode() != code:
+                continue
+            if level is not None and str(self[key].level) != str(level):
+                # found a course with correct key but wrong level...
+                continue
+            del self[key]
+            self._p_changed = True
+        return
+
+    def moveCertificate(self, facname, depname, newcode):
+        self.moved = True
+        cert = self
+        oldcode = cert.code
+        cert.code = newcode
+        del self.__parent__[oldcode]
+        newdep = grok.getSite()['faculties'][facname][depname]
+        newdep.certificates[newcode] = cert
+        #self.__parent__._p_changed = True
+        cat = getUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=(oldcode, oldcode))
+        for student in results:
+            notify(grok.ObjectModifiedEvent(student))
+            student.__parent__.logger.info(
+                '%s - Certificate moved' % student.__name__)
+        return
+
+class CertificateFactory(grok.GlobalUtility):
+    """A factory for certificates.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Certificate')
+    title = u"Create a new certificate.",
+    description = u"This factory instantiates new certificate instances."
+
+    def __call__(self, *args, **kw):
+        return Certificate(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(Certificate)
+
+class CertificateCourse(grok.Model):
+    grok.implements(ICertificateCourse)
+
+    def __init__(self, course=None, level=100,
+                 mandatory=True, course_category=None):
+        self.course = course
+        self.level = level
+        self.mandatory = mandatory
+        self.course_category = course_category
+
+    def getCourseCode(self):
+        """Get code of a course.
+        """
+        return self.course.code
+
+    @property
+    def longtitle(self):
+        return "%s in level %s" % (self.course.code,
+                   course_levels.getTerm(self.level).title)
+
+class CertificateCourseFactory(grok.GlobalUtility):
+    """A factory for certificate courses.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CertificateCourse')
+    title = u"Create a new certificate course.",
+    description = u"This factory instantiates new certificate courses."
+
+    def __call__(self, *args, **kw):
+        return CertificateCourse(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CertificateCourse)
+
+@grok.subscribe(ICertificate, grok.IObjectRemovedEvent)
+def handle_certificate_removed(certificate, event):
+    """If a certificate is deleted, we make sure that also referrers to
+    student studycourse objects are removed.
+    """
+    # Do not remove referrer if certificate is going to move
+    if getattr(certificate, 'moved', False):
+        return
+
+    code = certificate.code
+
+    # Find all student studycourses that refer to given certificate...
+    try:
+        cat = getUtility(ICatalog, name='students_catalog')
+    except ComponentLookupError:
+        # catalog not available. This might happen during tests.
+        return
+
+    results = cat.searchResults(certcode=(code, code))
+    for student in results:
+        # Remove that referrer...
+        studycourse = student['studycourse']
+        studycourse.certificate = None
+        notify(grok.ObjectModifiedEvent(student))
+        student.__parent__.logger.info(
+            'ObjectRemovedEvent - %s - removed: certificate' % student.__name__)
+    return
+
+class CertificatesPlugin(grok.GlobalUtility):
+    """A plugin that updates certificates.
+    """
+
+    grok.implements(IKofaPluggable)
+    grok.name('certificates')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        cat = getUtility(ICatalog, name='certificates_catalog')
+        results = cat.apply({'code':(None,None)})
+        uidutil = getUtility(IIntIds, context=cat)
+        items = getFields(ICertificate).items()
+        for r in results:
+            o = uidutil.getObject(r)
+            # Add new attributes
+            for i in items:
+                if not hasattr(o,i[0]):
+                    setattr(o,i[0],i[1].missing_value)
+                    logger.info(
+                        'CertificatesPlugin: %s attribute %s added.' % (
+                        o.code,i[0]))
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(o,i)
+                    logger.info(
+                        'CertificatesPlugin: %s attribute %s deleted.' % (
+                        o.code,i))
+                except AttributeError:
+                    pass
+        return
+
+class CertificateCoursesPlugin(grok.GlobalUtility):
+    """A plugin that updates certificate courses.
+    """
+
+    grok.implements(IKofaPluggable)
+    grok.name('certcourses')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        cat = getUtility(ICatalog, name='certcourses_catalog')
+        results = cat.apply({'course_code':(None,None)})
+        uidutil = getUtility(IIntIds, context=cat)
+        items = getFields(ICertificateCourse).items()
+        for r in results:
+            o = uidutil.getObject(r)
+            # Add new attributes
+            for i in items:
+                if not hasattr(o,i[0]):
+                    setattr(o,i[0],i[1].missing_value)
+                    logger.info(
+                        'CertificateCoursesPlugin: %s/%s_%s attribute %s added.' % (
+                        o.__parent__.code,o.course.code,o.level,i[0]))
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(o,i)
+                    logger.info(
+                        'CertificateCoursesPlugin: %s/%s_%s attribute %s deleted.' % (
+                        o.__parent__.code,o.course.code,o.level,i))
+                except AttributeError:
+                    pass
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificatescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificatescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/certificatescontainer.py	(revision 16045)
@@ -0,0 +1,130 @@
+## $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
+##
+"""Containers for certificates.
+"""
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component.interfaces import IFactory
+from zope.component import queryUtility
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import DuplicationError
+from waeup.kofa.university.interfaces import (
+    ICertificatesContainer, ICertificate)
+
+class CertificatesContainer(grok.Container):
+    """A storage for certificates.
+
+    A :class:`CertificatesContainer` stores
+    :class:`waeup.kofa.university.Certificate` instances.
+
+    It is a :class:`grok.Container` basically acting like a standard
+    Python dictionary. That means you can add certificates like items
+    in a normal dictionary and also get certificates by using
+    :meth:`values`, :meth:`keys`, and :meth:`items`.
+
+    This container type is picky about its contents: only real
+    certificates can be stored here and each new certificate must
+    provide a unique `code`. See :meth:`addCertificate` for details.
+
+    Each :class:`CertificatesContainer` provides
+    :class:`ICertificatesContainer`.
+    """
+    grok.implements(ICertificatesContainer)
+
+    def __setitem__(self, name, certificate):
+        """Insert `certificate` with `name` as key into container.
+
+        The `certificate` must be an object implementing
+        :class:`waeup.kofa.university.interfaces.ICertificate`. If
+        not, a :exc:`TypeError` is raised.
+
+        If the certificate `code` does not equal `name` a
+        :exc:`ValueError` is raised.
+
+        If the `code` attribute of `certificate` is already in use by
+        another certificate stored in the local site
+        (:class:`waeup.kofa.app.University` instance), then a
+        :exc:`waeup.kofa.interfaces.DuplicationError` will be raised.
+
+        If `name` is already used as a key, a :exc:`KeyError` will be
+        raised.
+        """
+        if not ICertificate.providedBy(certificate):
+            raise TypeError('CertificatesContainers contain only '
+                            'ICertificate instances')
+
+        # Only accept certs with code == key.
+        if certificate.code != name:
+            raise ValueError('key must match certificate code: '
+                             '%s, %s' % (name, certificate.code))
+
+        # Lookup catalog. If we find none: no duplicates possible.
+        cat = queryUtility(ICatalog, name='certificates_catalog', default=None)
+        if cat is not None:
+            entries = cat.searchResults(
+                code=(certificate.code,certificate.code))
+            if len(entries) > 0:
+                raise DuplicationError(
+                    'Certificate exists already elsewhere.', entries)
+        else:
+            # No catalog, then this addition won't do harm to anything.
+            pass
+        super(CertificatesContainer, self).__setitem__(name, certificate)
+
+    def addCertificate(self, certificate):
+        """Add `certificate` to the container.
+
+        The certificate must be an object implementing
+        :class:`waeup.kofa.university.interfaces.ICertificate`. If
+        not, a :exc:`TypeError` is raised.
+
+        The certificate will be stored in the container with its
+        `code` attribute as key. If this key is already used for
+        another certificate stored in the local site
+        (:class:`waeup.kofa.app.University` instance), then a
+        :exc:`waeup.kofa.interfaces.DuplicationError` will be raised.
+        """
+        self[getattr(certificate, 'code', None)] = certificate
+
+    def clear(self):
+        """Remove all contents from the certificate container.
+
+        This methods is pretty fast and optimized. Use it instead of
+        removing all items manually yourself.
+        """
+        # This internal function is implemented in C and thus much
+        # faster as we could do it in pure Python.
+        self._SampleContainer__data.clear()
+        # The length attribute is 'lazy'. See `zope.container` for details.
+        # This way we make sure, the next time len() is called, it returns
+        # the real value and not a cached one.
+        del self.__dict__['_BTreeContainer__len']
+
+class CertificatesContainerFactory(grok.GlobalUtility):
+    """A factory for certificate containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CertificatesContainer')
+    title = u"Create a new container for certificates.",
+    description = u"This factory instantiates new containers for certificates."
+
+    def __call__(self, *args, **kw):
+        return CertificatesContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CertificatesContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/course.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/course.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/course.py	(revision 16045)
@@ -0,0 +1,159 @@
+## $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
+##
+"""Courses.
+"""
+import grok
+import zope.location.location
+from zope.catalog.interfaces import ICatalog
+from zope.interface import implementedBy
+from zope.schema import getFields
+from zope.intid.interfaces import IIntIds
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.university.interfaces import ICourse
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+
+class VirtualCourseExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for courses.
+    """
+
+class Course(grok.Model):
+    """A university course.
+    """
+    grok.implements(ICourse)
+
+    local_roles = ['waeup.local.Lecturer']
+
+    def __init__(self,
+                 title=u'Unnamed Course',
+                 code=u'NA',
+                 credits=0,
+                 passmark=40,
+                 semester=1,
+                 former_course=False,
+                 **kw):
+        super(Course, self).__init__(**kw)
+        self.title = title
+        self.code = code
+        self.credits = credits
+        self.passmark = passmark
+        self.semester = semester
+        self.former_course = former_course
+        self.results_validated_by = None
+        self.results_validation_date = None
+        self.results_validation_session = None
+        self.score_editing_disabled = False
+
+    def traverse(self, name):
+        """Deliver appropriate containers.
+        """
+        if name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualCourseExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+    @property
+    def longtitle(self):
+        return "%s (%s)" % (self.title,self.code)
+
+class CourseFactory(grok.GlobalUtility):
+    """A factory for courses.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Course')
+    title = u"Create a new course.",
+    description = u"This factory instantiates new course instances."
+
+    def __call__(self, *args, **kw):
+        return Course(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(Course)
+
+@grok.subscribe(ICourse, grok.IObjectRemovedEvent)
+def handle_course_removed(course, event):
+    """If a course is deleted, we make sure that also referrers in a
+       certificatescontainer are removed.
+    """
+    code = course.code
+
+    # Find all certificatecourses that refer to given course
+    try:
+        cat = getUtility(ICatalog, name='certcourses_catalog')
+    except ComponentLookupError:
+        # catalog not available. This might happen during tests.
+        return
+
+    results = cat.searchResults(course_code=(code, code))
+
+    # remove each found referrer (certs might refer to same course multiple
+    # times)
+    certs = [x.__parent__ for x in results]
+    unique_certs = list(set(certs))
+    for cert in unique_certs:
+        cert.delCertCourses(code)
+    return
+
+@grok.subscribe(ICourse, grok.IObjectModifiedEvent)
+def handle_set_former_course(course, event):
+    """If a former course attribute is set, we make sure that referrers in a
+       certificatescontainer are removed.
+    """
+    if event.object.former_course:
+        handle_course_removed(course, event)
+    return
+
+class CoursesPlugin(grok.GlobalUtility):
+    """A plugin that updates courses.
+    """
+
+    grok.implements(IKofaPluggable)
+    grok.name('courses')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        cat = getUtility(ICatalog, name='courses_catalog')
+        results = cat.apply({'code':(None,None)})
+        uidutil = getUtility(IIntIds, context=cat)
+        items = getFields(ICourse).items()
+        for r in results:
+            o = uidutil.getObject(r)
+            # Add new attributes
+            for i in items:
+                if not hasattr(o,i[0]):
+                    setattr(o,i[0],i[1].missing_value)
+                    logger.info(
+                        'CoursesPlugin: %s attribute %s added.' % (
+                        o.code,i[0]))
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(o,i)
+                    logger.info(
+                        'CoursesPlugin: %s attribute %s deleted.' % (
+                        o.code,i))
+                except AttributeError:
+                    pass
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/coursescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/coursescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/coursescontainer.py	(revision 16045)
@@ -0,0 +1,81 @@
+## $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
+##
+"""Course containers.
+"""
+import grok
+from zope.interface import implementedBy
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.kofa.interfaces import DuplicationError
+from waeup.kofa.university.interfaces import ICoursesContainer, ICourse
+
+class CoursesContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(ICoursesContainer)
+
+    def __setitem__(self, name, course):
+        """See corresponding docstring in certificatescontainer.py.
+        """
+        if not ICourse.providedBy(course):
+            raise TypeError('CoursesContainers contain only '
+                            'ICourse instances')
+
+        # Only accept courses with code == key.
+        if course.code != name:
+            raise ValueError('key must match course code: '
+                             '%s, %s' % (name, course.code))
+
+        # Lookup catalog. If we find none: no duplicates possible.
+        cat = queryUtility(ICatalog, name='courses_catalog', default=None)
+        if cat is not None:
+            entries = cat.searchResults(
+                code=(course.code,course.code))
+            if len(entries) > 0:
+                raise DuplicationError(
+                    'Course exists already elsewhere.', entries)
+        else:
+            # No catalog, then this addition won't do harm to anything.
+            pass
+        super(CoursesContainer, self).__setitem__(name, course)
+
+    def addCourse(self, course):
+        """See corresponding docstring in certificatescontainer.py.
+        """
+        self[getattr(course, 'code', None)] = course
+
+    def clear(self):
+        """See corresponding docstring and comments in certificatescontainer.py.
+        """
+        self._SampleContainer__data.clear()
+        del self.__dict__['_BTreeContainer__len']
+
+class CoursesContainerFactory(grok.GlobalUtility):
+    """A factory for course containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CoursesContainer')
+    title = u"Create a new container for courses.",
+    description = u"This factory instantiates new containers for courses."
+
+    def __call__(self, *args, **kw):
+        return CoursesContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CoursesContainer)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/department.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/department.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/department.py	(revision 16045)
@@ -0,0 +1,177 @@
+## $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
+##
+"""University departments.
+"""
+import grok
+import zope.location.location
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.component import getUtility
+from zope.schema import getFields
+from waeup.kofa.university.faculty import longtitle
+from waeup.kofa.university.coursescontainer import CoursesContainer
+from waeup.kofa.university.certificatescontainer import CertificatesContainer
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable
+from waeup.kofa.university.interfaces import IDepartment
+
+class VirtualDepartmentExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for departments.
+    """
+
+class Department(grok.Container):
+    """A university department.
+    """
+    grok.implements(IDepartment)
+
+    local_roles = [
+        'waeup.local.ApplicationsManager',
+        'waeup.local.DepartmentOfficer',
+        'waeup.local.DepartmentManager',
+        'waeup.local.ClearanceOfficer',
+        'waeup.local.UGClearanceOfficer',
+        'waeup.local.PGClearanceOfficer',
+        'waeup.local.ReportsOfficer',
+        'waeup.local.CourseAdviser100',
+        'waeup.local.CourseAdviser200',
+        'waeup.local.CourseAdviser300',
+        'waeup.local.CourseAdviser400',
+        'waeup.local.CourseAdviser500',
+        'waeup.local.CourseAdviser600',
+        'waeup.local.CourseAdviser700',
+        'waeup.local.CourseAdviser800',
+        'waeup.local.LocalStudentsManager',
+        'waeup.local.Lecturer',
+        ]
+
+    def __init__(self,
+                 title=u'Unnamed Department',
+                 title_prefix=u'department',
+                 code=u"NA",
+                 officer_1=None,
+                 officer_2=None,
+                 officer_3=None,
+                 officer_4=None,
+                 **kw):
+        super(Department, self).__init__(**kw)
+        self.title = title
+        self.title_prefix = title_prefix
+        self.officer_1 = officer_1
+        self.officer_2 = officer_2
+        self.officer_3 = officer_3
+        self.officer_4 = officer_4
+        self.code = code
+        self.courses = CoursesContainer()
+        self.courses.__parent__ = self
+        self.courses.__name__ = 'courses'
+        self.certificates = CertificatesContainer()
+        self.certificates.__parent__ = self
+        self.certificates.__name__ = 'certificates'
+        self.score_editing_disabled = False
+
+    def traverse(self, name):
+        """Deliver appropriate containers, if someone wants to go to courses,
+        certificates or exports.
+        """
+        if name == 'courses':
+            return self.courses
+        elif name == 'certificates':
+            return self.certificates
+        elif name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualDepartmentExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+    @property
+    def longtitle(self):
+        return longtitle(self)
+
+    def moveDepartment(self, facname, depname):
+        """ Move department to new department named depname in
+        faculty named facname.
+
+        """
+        self.moved = True
+        newfac = grok.getSite()['faculties'][facname]
+        oldcode = self.code
+        oldfac = self.__parent__
+        newfac[depname] = self
+        del oldfac[oldcode]
+        newfac[depname].code = depname
+        #self.__parent__._p_changed = True
+        cat = getUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(depcode=(oldcode, oldcode))
+        for student in results:
+            notify(grok.ObjectModifiedEvent(student))
+            student.__parent__.logger.info(
+                '%s - Department moved' % student.__name__)
+        return
+
+class DepartmentFactory(grok.GlobalUtility):
+    """A factory for department containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Department')
+    title = u"Create a new department.",
+    description = u"This factory instantiates new department instances."
+
+    def __call__(self, *args, **kw):
+        return Department(*args, **kw)
+
+    def getInterfaces(self):
+        """Get interfaces of objects provided by this factory.
+        """
+        return implementedBy(Department)
+
+class DepartmentsPlugin(grok.GlobalUtility):
+    """A plugin that updates departments.
+    """
+
+    grok.implements(IKofaPluggable)
+    grok.name('departments')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        items = getFields(IDepartment).items()
+        for faculty in site['faculties'].values():
+            for department in faculty.values():
+                # Add new attributes
+                for i in items:
+                    if not hasattr(department,i[0]):
+                        setattr(department,i[0],i[1].missing_value)
+                        logger.info(
+                            'DepartmentsPlugin: %s attribute %s added.' % (
+                            department.code,i[0]))
+                # Remove deprecated attributes
+                for i in self.deprecated_attributes:
+                    try:
+                        delattr(department,i)
+                        logger.info(
+                            'DepartmentsPlugin: %s attribute %s deleted.' % (
+                            department.code,i))
+                    except AttributeError:
+                        pass
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/export.py	(revision 16045)
@@ -0,0 +1,248 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""Exporters for faculties, departments, and other academics components.
+"""
+import grok
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import ExporterBase
+
+
+class FacultyExporter(grok.GlobalUtility, ExporterBase):
+    """The Faculty Exporter exports all faculties in the `faculties`
+    container. This is the only place where faculties are stored.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('faculties')
+
+    fields = ('code', 'title', 'title_prefix', 'users_with_local_roles',
+              'officer_1', 'officer_2')
+
+    title = _(u'Faculties')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler computes the `users_with_local_roles` value which
+        is a Python expression like:
+
+        ``[{'user_name': u'bob', 'local_role': u'bobsrole'},
+        {'user_name': u'anna', 'local_role': u'annasrole'}]``
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in \
+                role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        return super(FacultyExporter, self).mangle_value(
+            value, name, context)
+
+    def export(self, faculties, filepath=None):
+        """Export `faculties`, an iterable, as CSV file.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for faculty in faculties:
+            self.write_item(faculty, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export faculties in facultycontainer into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        faculties = site.get('faculties', {})
+        return self.export(faculties.values(), filepath)
+
+
+class DepartmentExporter(FacultyExporter, grok.GlobalUtility):
+    """The Department Exporter exports all departments stored in the faculty
+    containers. The exporter iterates over all faculties and then over all
+    departments inside each faculty container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('departments')
+
+    fields = ('code', 'faculty_code', 'title', 'title_prefix',
+              'users_with_local_roles',
+              'officer_1', 'officer_2','officer_3', 'officer_4')
+
+    title = _(u'Departments')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler additionally computes the faculty_code value
+        which is the code (= object id) of the faculty that hosts
+        the department.
+        """
+        if name == 'faculty_code':
+            value = getattr(
+                getattr(context, '__parent__', None),
+                'code', None)
+        return super(DepartmentExporter, self).mangle_value(
+            value, name, context)
+
+    def export_all(self, site, filepath=None):
+        """Export departments in faculty into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        faculties = site.get('faculties', {})
+        for faculty in faculties.values():
+            for department in faculty.values():
+                self.write_item(department, writer)
+        return self.close_outfile(filepath, outfile)
+
+
+class CourseExporter(FacultyExporter, grok.GlobalUtility):
+    """The Course Exporter exports all courses in the database. It iterates
+    over all departments and faculties.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('courses')
+
+    fields = ('code', 'faculty_code', 'department_code', 'title', 'credits',
+              'passmark', 'semester', 'users_with_local_roles', 'former_course',
+              'results_validated_by', 'results_validation_date',
+              'results_validation_session')
+
+    title = _(u'Courses')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler additionally computes the department_code value
+        which is the code of the department that offers the course.
+        """
+        if name == 'users_with_local_roles':
+            value = []
+            role_map = IPrincipalRoleMap(context)
+            for local_role, user_name, setting in \
+                role_map.getPrincipalsAndRoles():
+                value.append({'user_name':user_name,'local_role':local_role})
+        elif name == 'faculty_code':
+            try:
+                value = context.__parent__.__parent__.__parent__.code
+            except AttributeError:
+                value = None
+        elif name == 'department_code':
+            try:
+                value = context.__parent__.__parent__.code
+            except AttributeError:
+                value = None
+        return super(CourseExporter, self).mangle_value(
+            value, name, context)
+
+    def export_all(self, site, filepath=None):
+        """Export courses into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        faculties = site.get('faculties', {})
+        for faculty in faculties.values():
+            for department in faculty.values():
+                for course in department.courses.values():
+                    self.write_item(course, writer)
+        return self.close_outfile(filepath, outfile)
+
+
+class CertificateExporter(CourseExporter, grok.GlobalUtility):
+    """The Certificate Exporter exports all certificates in the database.
+    It iterates over all departments and faculties.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('certificates')
+
+    fields = ('code', 'faculty_code', 'department_code', 'title', 'study_mode',
+              'degree',
+              'start_level', 'end_level', 'application_category', 'ratio',
+              'school_fee_1', 'school_fee_2', 'school_fee_3', 'school_fee_4',
+              'custom_textline_1', 'custom_textline_2',
+              'custom_float_1', 'custom_float_2',
+              'users_with_local_roles')
+
+    title = _(u'Certificates')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler additionally computes the department_code value
+        which is the code of the department that offers the certificate.
+        """
+        return super(CertificateExporter, self).mangle_value(
+            value, name, context)
+
+    def export_all(self, site, filepath=None):
+        """Export certificates into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        faculties = site.get('faculties', {})
+        for faculty in faculties.values():
+            for department in faculty.values():
+                for cert in department.certificates.values():
+                    self.write_item(cert, writer)
+        return self.close_outfile(filepath, outfile)
+
+
+class CertificateCourseExporter(CourseExporter, grok.GlobalUtility):
+    """The Certificate Course Exporter exports all certificate courses
+    (:class:`CertificateCourse
+    <waeup.kofa.university.certificate.CertificateCourse>` instances) in
+    the database. It iterates over all departments and faculties.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('certificate_courses')
+
+    fields = ('course', 'faculty_code', 'department_code', 'certificate_code',
+              'level', 'mandatory', 'course_category')
+
+    title = _(u'Courses in Certificates')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler computes the codes of the faculty, the department and
+        the certificate which require the course. It also exports the
+        course code.
+
+        .. note:: The course must not necessarily be offered by the same
+                  department.
+        """
+        if name == 'faculty_code':
+            try:
+                value = context.__parent__.__parent__.__parent__.__parent__.code
+            except AttributeError:
+                value = None
+        elif name == 'department_code':
+            try:
+                value = context.__parent__.__parent__.__parent__.code
+            except AttributeError:
+                value = None
+        elif name == 'certificate_code':
+            value = getattr(context, '__parent__', None)
+            value = getattr(value, 'code', None)
+        if name == 'course':
+            value = getattr(value, 'code', None)
+        return super(CourseExporter, self).mangle_value(
+            value, name, context)
+
+    def export_all(self, site, filepath=None):
+        """Export certificate courses into filepath as CSV data.
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        faculties = site.get('faculties', {})
+        for faculty in faculties.values():
+            for department in faculty.values():
+                for cert in department.certificates.values():
+                    for certref in cert.values():
+                        self.write_item(certref, writer)
+        return self.close_outfile(filepath, outfile)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/facultiescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/facultiescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/facultiescontainer.py	(revision 16045)
@@ -0,0 +1,81 @@
+## $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
+##
+import grok
+import zope.location.location
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import IKofaPluggable
+from waeup.kofa.university.interfaces import IFacultiesContainer, IFaculty
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+
+class VirtualFacultiesExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for the faculties container.
+    """
+
+class FacultiesContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(IFacultiesContainer)
+
+    def traverse(self, name):
+        if name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualFacultiesExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+    def addFaculty(self, faculty):
+        if not IFaculty.providedBy(faculty):
+            raise TypeError(
+                'FacultiesContainers contain only IFaculty instances')
+        self[faculty.code] = faculty
+        return
+
+class FacultiesContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.FacultiesContainer')
+    title = u"Create a new facultiescontainer.",
+    description = u"This factory instantiates new containers for faculties."
+
+    def __call__(self, *args, **kw):
+        return FacultiesContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(FacultiesContainer)
+
+class AcademicsPlugin(grok.GlobalUtility):
+    """A plugin that creates container for faculties inside a university.
+    """
+    grok.implements(IKofaPluggable)
+    grok.name('academics')
+
+    def setup(self, site, name, logger):
+        if 'faculties' in site.keys():
+            logger.warn('Could not create container for faculties in Kofa.')
+            return
+        site['faculties'] = FacultiesContainer()
+        logger.info('Container for faculties created')
+        return
+
+    def update(self, site, name, logger):
+        if not 'faculties' in site.keys():
+            self.setup(site, name, logger)
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/faculty.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/faculty.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/faculty.py	(revision 16045)
@@ -0,0 +1,149 @@
+## $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
+##
+"""Faculty components.
+"""
+
+import grok
+import zope.location.location
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.component import getUtility
+from zope.schema import getFields
+from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable
+from waeup.kofa.utils.batching import VirtualExportJobContainer
+from waeup.kofa.university.interfaces import (
+    IFaculty, IDepartment)
+
+def longtitle(inst):
+    insttypes_dict = getUtility(IKofaUtils).INST_TYPES_DICT
+    if inst.title_prefix == 'none':
+        return "%s (%s)" % (inst.title, inst.code)
+    return "%s %s (%s)" % (
+        insttypes_dict[inst.title_prefix],
+        inst.title, inst.code)
+
+class VirtualFacultyExportJobContainer(VirtualExportJobContainer):
+    """A virtual export job container for facultiess.
+    """
+
+class Faculty(grok.Container):
+    """A university faculty.
+    """
+    grok.implements(IFaculty)
+
+    local_roles = [
+        'waeup.local.DepartmentOfficer',
+        'waeup.local.DepartmentManager',
+        'waeup.local.ClearanceOfficer',
+        'waeup.local.UGClearanceOfficer',
+        'waeup.local.PGClearanceOfficer',
+        'waeup.local.CourseAdviser100',
+        'waeup.local.CourseAdviser200',
+        'waeup.local.CourseAdviser300',
+        'waeup.local.CourseAdviser400',
+        'waeup.local.CourseAdviser500',
+        'waeup.local.CourseAdviser600',
+        'waeup.local.CourseAdviser700',
+        'waeup.local.CourseAdviser800',
+        'waeup.local.LocalStudentsManager',
+        'waeup.local.LocalWorkflowManager',
+        'waeup.local.TranscriptOfficer',
+        'waeup.local.TranscriptSignee',
+        ]
+
+    def __init__(self,
+                 title=u'Unnamed Faculty',
+                 title_prefix=u'faculty',
+                 code=u'NA',
+                 officer_1=None,
+                 officer_2=None,
+                 **kw):
+        super(Faculty, self).__init__(**kw)
+        self.title = title
+        self.title_prefix = title_prefix
+        self.code = code
+        self.officer_1 = officer_1
+        self.officer_2 = officer_2
+
+    def traverse(self, name):
+        """Deliver appropriate containers.
+        """
+        if name == 'exports':
+            # create a virtual exports container and return it
+            container = VirtualFacultyExportJobContainer()
+            zope.location.location.located(container, self, 'exports')
+            return container
+        return None
+
+    def addDepartment(self, department):
+        """Add a department to the faculty.
+        """
+        if not IDepartment.providedBy(department):
+            raise TypeError('Faculties contain only IDepartment instances')
+        self[department.code] = department
+
+    @property
+    def longtitle(self):
+        return longtitle(self)
+
+class FacultyFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.Faculty')
+    title = u"Create a new faculty.",
+    description = u"This factory instantiates new faculty instances."
+
+    def __call__(self, *args, **kw):
+        return Faculty()
+
+    def getInterfaces(self):
+        return implementedBy(Faculty)
+
+class FacultiesPlugin(grok.GlobalUtility):
+    """A plugin that updates faculties.
+    """
+
+    grok.implements(IKofaPluggable)
+    grok.name('faculties')
+
+    deprecated_attributes = []
+
+    def setup(self, site, name, logger):
+        return
+
+    def update(self, site, name, logger):
+        items = getFields(IFaculty).items()
+        for faculty in site['faculties'].values():
+             # Add new attributes
+            for i in items:
+                if not hasattr(faculty,i[0]):
+                    setattr(faculty,i[0],i[1].missing_value)
+                    logger.info(
+                        'FacultiesPlugin: %s attribute %s added.' % (
+                        faculty.code,i[0]))
+            # Remove deprecated attributes
+            for i in self.deprecated_attributes:
+                try:
+                    delattr(faculty,i)
+                    logger.info(
+                        'FacultiesPlugin: %s attribute %s deleted.' % (
+                        faculty.code,i))
+                except AttributeError:
+                    pass
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/interfaces.py	(revision 16045)
@@ -0,0 +1,375 @@
+## $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
+##
+"""Interfaces of academics specific objects.
+"""
+
+from zope import schema
+from zope.interface import Attribute, invariant, Invalid
+from waeup.kofa.interfaces import (
+    IKofaObject, IKofaContainer, validate_id, academic_sessions_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.university.vocabularies import (
+    course_levels,
+    CourseSource,
+    StudyModeSource,
+    AppCatSource,
+    InstTypeSource,
+    SemesterSource,
+    DegreeSource,
+    CourseCategorySource
+    )
+
+class IFaculty(IKofaContainer):
+    """Representation of a university faculty.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Name of faculty'),
+        default = u'Unnamed',
+        required = True,
+        )
+
+    title_prefix = schema.Choice(
+        title = _(u'Name prefix'),
+        default = u'faculty',
+        source = InstTypeSource(),
+        required = True,
+        )
+
+    officer_1 = schema.TextLine(
+        title = _(u'Faculty Officer 1'),
+        default = u'',
+        required = False,
+        )
+
+    officer_2 = schema.TextLine(
+        title = _(u'Faculty Officer 2'),
+        default = u'',
+        required = False,
+        )
+
+
+class IFacultiesContainer(IKofaContainer):
+    """A container for faculties.
+    """
+    def addFaculty(faculty):
+        """Add an IFactulty object.
+
+        """
+class IDepartment(IKofaObject):
+    """Representation of a department.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Name of department'),
+        default = u'Unnamed',
+        required = True,
+        )
+
+    title_prefix = schema.Choice(
+        title = _(u'Name prefix'),
+        source = InstTypeSource(),
+        default = u'department',
+        required = True,
+        )
+
+    score_editing_disabled = schema.Bool(
+        title = _(u'Score editing disabled'),
+        description = _(
+            u'Lectures can not edit scores if ticked.'),
+        required = False,
+        default = False,
+        )
+
+    officer_1 = schema.TextLine(
+        title = _(u'Department Officer 1'),
+        default = u'',
+        required = False,
+        )
+
+    officer_2 = schema.TextLine(
+        title = _(u'Department Officer 2'),
+        default = u'',
+        required = False,
+        )
+
+    officer_3 = schema.TextLine(
+        title = _(u'Department Officer 3'),
+        default = u'',
+        required = False,
+        )
+
+    officer_4 = schema.TextLine(
+        title = _(u'Department Officer 4'),
+        default = u'',
+        required = False,
+        )
+
+    courses = Attribute("A container for courses.")
+    certificates = Attribute("A container for certificates.")
+
+
+class ICoursesContainer(IKofaContainer):
+    """A container for faculties.
+    """
+    def addCourse(course):
+        """Add an ICourse object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICourse(IKofaObject):
+    """Representation of a course.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title of course'),
+        default = u'Unnamed',
+        required = True,
+        )
+
+    credits = schema.Int(
+        title = _(u'Credits'),
+        default = 0,
+        required = True,
+        )
+
+    passmark = schema.Int(
+        title = _(u'Passmark'),
+        default = 40,
+        required = True,
+        )
+
+    semester = schema.Choice(
+        title = _(u'Semester/Term'),
+        default = 9,
+        source = SemesterSource(),
+        required = True,
+        )
+
+    former_course = schema.Bool(
+        title = _(u'Former course'),
+        description = _(
+            u'If this attribute is being set all certificate courses '
+            'referring to this course will be automatically deleted.'),
+        required = False,
+        default = False,
+        )
+
+    results_validated_by = schema.TextLine(
+        title = _(u'Results validated by'),
+        default = None,
+        required = False,
+        )
+
+    results_validation_date = schema.Datetime(
+        title = _(u'Results validation date'),
+        required = False,
+        readonly = False,
+        )
+
+    results_validation_session = schema.Choice(
+        title = _(u'Results validation session'),
+        source = academic_sessions_vocab,
+        required = False,
+        readonly = False,
+        )
+
+    score_editing_disabled = schema.Bool(
+        title = _(u'Score editing disabled'),
+        description = _(
+            u'Lectures can not edit scores if ticked.'),
+        required = False,
+        default = False,
+        )
+
+class ICertificate(IKofaObject):
+    """Representation of a certificate.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        constraint=validate_id,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        default = u'Unnamed',
+        required = True,
+        )
+
+    study_mode = schema.Choice(
+        title = _(u'Study Mode'),
+        source = StudyModeSource(),
+        default = u'ug_ft',
+        required = True,
+        )
+
+    degree = schema.Choice(
+        title = _(u'Degree'),
+        source = DegreeSource(),
+        required = False,
+        )
+
+    start_level = schema.Choice(
+        title = _(u'Start Level'),
+        vocabulary = course_levels,
+        default = 100,
+        required = True,
+        )
+
+    end_level = schema.Choice(
+        title = _(u'End Level'),
+        vocabulary = course_levels,
+        default = 500,
+        required = True,
+        )
+
+    application_category = schema.Choice(
+        title = _(u'Application Category'),
+        source = AppCatSource(),
+        default = u'basic',
+        required = True,
+        )
+
+    school_fee_1 = schema.Float(
+        title = _(u'Initial School Fee'),
+        required = False,
+        default = 0.0,
+        )
+
+    school_fee_2 = schema.Float(
+        title = _(u'Returning School Fee'),
+        required = False,
+        default = 0.0,
+        )
+
+    school_fee_3 = schema.Float(
+        title = _(u'Foreigner Initial School Fee'),
+        required = False,
+        default = 0.0,
+        )
+
+    school_fee_4 = schema.Float(
+        title = _(u'Foreigner Returning School Fee'),
+        required = False,
+        default = 0.0,
+        )
+
+    ratio = schema.Float(
+        title = _(u'Installment Ratio'),
+        required = False,
+        min = 0.0,
+        max = 1.0,
+        )
+
+    custom_textline_1 = schema.TextLine(
+        title = _(u'Custom Textline 1 (not used)'),
+        required = False,
+        )
+
+    custom_textline_2 = schema.TextLine(
+        title = _(u'Custom Textline 2 (not used)'),
+        required = False,
+        )
+
+    custom_float_1 = schema.Float(
+        title = _(u'Custom Float 1 (not used)'),
+        required = False,
+        )
+
+    custom_float_2 = schema.Float(
+        title = _(u'Custom Float 2 (not used)'),
+        required = False,
+        )
+
+    @invariant
+    def check_pg_conditions(cert):
+        if cert.start_level == 999 and not cert.end_level == 999:
+            raise Invalid(_("Start level and end level must correspond."))
+        if cert.end_level == 999 and not cert.start_level == 999:
+            raise Invalid(_("Start level and end level must correspond."))
+        if cert.study_mode.startswith('pg') and not cert.start_level == 999:
+            raise Invalid(_(
+                "Study mode, start level and end level must correspond."))
+        if cert.start_level == 999  and not cert.study_mode.startswith('pg'):
+            raise Invalid(_(
+                "Study mode, start level and end level must correspond."))
+
+
+class ICertificatesContainer(IKofaContainer):
+    """A container for certificates.
+    """
+    def addCertificate(certificate):
+        """Add an ICertificate object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICertificateCourse(IKofaObject):
+    """A certificatecourse is referring a course and provides some own
+       attributes.
+    """
+    course = schema.Choice(
+        title = _(u'Course'),
+        source = CourseSource(),
+        )
+
+    level = schema.Choice(
+        title = _(u'Level'),
+        required = True,
+        vocabulary = course_levels,
+        readonly = False,
+        )
+
+    course_category = schema.Choice(
+        title = _(u'Course Category'),
+        source = CourseCategorySource(),
+        required = False,
+        )
+
+    mandatory = schema.Bool(
+        title = _(u'Registration required'),
+        required = False,
+        default = True,
+        )
+
+    def getCourseCode():
+        """Return the code of the course referred to.
+
+        This is needed for cataloging.
+        """
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificate_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificate_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificate_data.csv	(revision 16045)
@@ -0,0 +1,8 @@
+faculty_code,department_code,code,local_roles
+FAC1,DEP1,CRT2,"[{'user_name':'anne','local_role':'waeup.local.CourseAdviser100'}]"
+FAC1,DEP1,CRT2,"[{'user_name':'alice','local_role':'waeup.local.CourseAdviser100'}]"
+FAC1,DEP1,CRT2,"[{'user_name':'anne','local_role':'waeup.local.Boss'}]"
+FAC1,DEP1,CRT2,"[('anything')]"
+FAC1,DEP1,CRT2,"["
+FAC1,DEP1,CRT2,"[{'user':'anne','local':'waeup.local.CourseAdviser100'}]"
+FAC11,DEP2,CRT2,"[{'user_name':'anne','local_role':'waeup.local.CourseAdviser100'}]"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificatecourse_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificatecourse_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_certificatecourse_data.csv	(revision 16045)
@@ -0,0 +1,6 @@
+certificate_code,course,department_code,faculty_code,level,mandatory
+CRT1,CRS1,DEP1,FAC1,200,1
+CRT1,CRS1,DEP1,FAC1,300,nonsense
+CRT1,CRS1,DEP1,FAC1,400,<IGNORE>
+CRT1,CRS1,DEP1,FAC1,100,
+CRT2,CRS1,DEP1,FAC1,100,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data.csv	(revision 16045)
@@ -0,0 +1,8 @@
+faculty_code,department_code,code,local_roles
+FAC1,DEP1,CRS2,"[{'user_name':'anne','local_role':'waeup.local.Lecturer'}]"
+FAC1,DEP1,CRS2,"[{'user_name':'alice','local_role':'waeup.local.Lecturer'}]"
+FAC1,DEP1,CRS2,"[{'user_name':'anne','local_role':'waeup.local.Boss'}]"
+FAC1,DEP1,CRS2,"[('anything')]"
+FAC1,DEP1,CRS2,"["
+FAC1,DEP1,CRS2,"[{'user':'anne','local':'waeup.local.Lecturer'}]"
+FAC11,DEP2,CRS2,"[{'user_name':'anne','local_role':'waeup.local.Lecturer'}]"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_course_data_update.csv	(revision 16045)
@@ -0,0 +1,2 @@
+code,title
+CRS2,"New Title"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_department_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_department_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_department_data.csv	(revision 16045)
@@ -0,0 +1,8 @@
+faculty_code,code,local_roles
+FAC1,DEP2,"[{'user_name':'anne','local_role':'waeup.local.DepartmentManager'}]"
+FAC1,DEP2,"[{'user_name':'alice','local_role':'waeup.local.DepartmentManager'}]"
+FAC1,DEP2,"[{'user_name':'anne','local_role':'waeup.local.Boss'}]"
+FAC1,DEP2,"[('anything')]"
+FAC1,DEP2,"["
+FAC1,DEP2,"[{'user':'anne','local':'waeup.local.DepartmentManager'}]"
+FAC11,DEP2,"[{'user_name':'anne','local_role':'waeup.local.DepartmentManager'}]"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data.csv	(revision 16045)
@@ -0,0 +1,7 @@
+code,local_roles
+ABC,"[{'user_name':'bob','local_role':'waeup.local.DepartmentManager'}]"
+CDE,"[{'user_name':'alice','local_role':'waeup.local.DepartmentManager'}]"
+DEF,"[{'user_name':'bob','local_role':'waeup.local.Boss'}]"
+EFG,"[('anything')]"
+FGH,"["
+GHI,"[{'user':'bob','local':'waeup.local.DepartmentManager'}]"
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data_update.csv
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data_update.csv	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/sample_faculty_data_update.csv	(revision 16045)
@@ -0,0 +1,5 @@
+code,local_roles
+FAC1,XXX
+FAC2,"[{'user_name':'bob','local_role':'waeup.local.DepartmentManager'}]"
+FAC3,<IGNORE>
+FAC4,
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,750 @@
+## $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
+##
+
+# Tests for university related batching
+import unittest
+import tempfile
+import shutil
+import os
+from zope.component.hooks import setSite, clearSite
+from zope.component import createObject
+from zope.securitypolicy.interfaces import (
+    IPrincipalRoleMap, IPrincipalRoleManager)
+from zope.testbrowser.testing import Browser
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.interfaces import IBatchProcessor
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.app import University
+from waeup.kofa.university.batching import (
+    FacultyProcessor, DepartmentProcessor, CourseProcessor,
+    CertificateProcessor, CertificateCourseProcessor)
+from waeup.kofa.university.certificate import Certificate, CertificateCourse
+from waeup.kofa.university.course import Course
+from waeup.kofa.university import Faculty, Department
+from waeup.kofa.university.batching import FacultyProcessor
+
+FACULTY_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_faculty_data.csv'),
+    'rb').read()
+
+FACULTY_HEADER_FIELDS = FACULTY_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+FACULTY_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_faculty_data_update.csv'),
+    'rb').read()
+
+FACULTY_HEADER_FIELDS_UPDATE = FACULTY_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+DEPARTMENT_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_department_data.csv'),
+    'rb').read()
+
+DEPARTMENT_HEADER_FIELDS = DEPARTMENT_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+CERTIFICATE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_certificate_data.csv'),
+    'rb').read()
+
+CERTIFICATE_HEADER_FIELDS = CERTIFICATE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+COURSE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_course_data.csv'),
+    'rb').read()
+
+COURSE_HEADER_FIELDS = COURSE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+COURSE_SAMPLE_DATA_UPDATE = open(
+    os.path.join(os.path.dirname(__file__), 'sample_course_data_update.csv'),
+    'rb').read()
+
+COURSE_HEADER_FIELDS_UPDATE = COURSE_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+
+CERTIFICATECOURSE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_certificatecourse_data.csv'),
+    'rb').read()
+
+CERTIFICATECOURSE_HEADER_FIELDS = CERTIFICATECOURSE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
+class UniversityProcessorSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UniversityProcessorSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(app)
+
+        self.app['users'].addUser('bob', 'bobssecret')
+        self.app['users'].addUser('anne', 'annessecret')
+
+        # Populate university
+        self.certificate = createObject('waeup.Certificate')
+        self.certificate.code = 'CRT1'
+        self.app['faculties']['FAC1'] = Faculty(code='FAC1')
+        self.app['faculties']['FAC1']['DEP1'] = Department(code='DEP1')
+        self.app['faculties']['FAC1']['DEP1'].certificates.addCertificate(
+            self.certificate)
+        self.course = createObject('waeup.Course')
+        self.course.code = 'CRS1'
+        self.app['faculties']['FAC1']['DEP1'].courses.addCourse(
+            self.course)
+        #self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
+        #    self.course, level=100)
+        
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        return
+
+    def tearDown(self):
+        super(UniversityProcessorSetup, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+class TestFacultyProcessor(UniversityProcessorSetup):
+
+    def setUp(self):
+        super(TestFacultyProcessor, self).setUp()
+
+        self.browser = Browser()
+        self.browser.handleErrors = False
+        self.datacenter_path = 'http://localhost/app/datacenter'
+        self.app['datacenter'].setStoragePath(self.dc_root)
+
+        self.proc = FacultyProcessor()
+        self.site1 = dict(faculties=dict())
+        self.site2 = dict(faculties=dict(FAC='pseudo faculty'))
+        self.row = dict(code='FAC')
+
+        self.csv_file_faculty = os.path.join(self.workdir, 'sample_faculty_data.csv')
+        open(self.csv_file_faculty, 'wb').write(FACULTY_SAMPLE_DATA)
+        self.csv_file_faculty_update = os.path.join(self.workdir, 'sample_faculty_data_update.csv')
+        open(self.csv_file_faculty_update, 'wb').write(FACULTY_SAMPLE_DATA_UPDATE)
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, FacultyProcessor)
+        verifyObject(IBatchProcessor, self.proc)
+        return
+
+    def test_get_entry(self):
+        # if a faculty exists already, we will get it
+        result1 = self.proc.getEntry(self.row, self.site1)
+        result2 = self.proc.getEntry(self.row, self.site2)
+        self.assertTrue(result1 is None)
+        self.assertEqual(result2, 'pseudo faculty')
+        return
+
+    def test_del_entry(self):
+        # make sure we can del entries.
+        self.proc.delEntry(self.row, self.site2)
+        self.assertTrue('FAC' not in self.site2.keys())
+        return
+
+    def test_checkConversion(self):
+        # Make sure we can check conversions.
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles='[]'))
+        self.assertEqual(len(errs),0)
+
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC',
+            local_roles="['nonsense'"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'Error')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC',
+            local_roles="('abc')"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'no list')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC',
+            local_roles="[('ABC')]"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'no dicts')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC',
+            local_roles="('abc')"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'no list')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles=
+            "[{'name':'bob','local_role':'waeup.local.DepartmentManager'},]"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'user_name or local_role missing')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles=
+            "[{'user_name':'bob','localrole':'waeup.local.DepartmentManager'},]"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'user_name or local_role missing')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles=
+            "[{'user_name':'bob','local_role':'waeup.local.Boss'},]"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'waeup.local.Boss not allowed')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles=
+            "[{'user_name':'john','local_role':'waeup.local.DepartmentManager'},]"
+            ))
+        self.assertEqual(len(errs),1)
+        self.assertEqual(errs, [('local_roles', 'john does not exist')])
+        errs, inv_errs, conv_dict = self.proc.checkConversion(
+            dict(faculty_code='ABC', local_roles=
+            "[{'user_name':'bob','local_role':'waeup.local.DepartmentManager'},]"
+            ))
+        self.assertEqual(len(errs),0)
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_faculty, FACULTY_HEADER_FIELDS)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,5)
+        self.assertEqual(
+            content,
+            'code,local_roles,--ERRORS--\r\n'
+            'CDE,"[{\'user_name\':\'alice\',\'local_role\':\'waeup.local.DepartmentManager\'}]",'
+            'local_roles: alice does not exist\r\n'
+            'DEF,"[{\'user_name\':\'bob\',\'local_role\':\'waeup.local.Boss\'}]",'
+            'local_roles: waeup.local.Boss not allowed\r\n'
+            'EFG,[(\'anything\')],local_roles: no dicts\r\n'
+            'FGH,[,local_roles: Error\r\n'
+            'GHI,"[{\'user\':\'bob\',\'local\':\'waeup.local.DepartmentManager\'}]",'
+            'local_roles: user_name or local_role missing\r\n'
+            )
+        # Bob got a local role in faculty ABC.
+        abc = self.app['faculties']['ABC']
+        role_map = IPrincipalRoleMap(abc)
+        local_role, user_name, setting = role_map.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'bob')
+        self.assertEqual(local_role, 'waeup.local.DepartmentManager')
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+    def test_import_update(self):
+        self.app['faculties']['FAC2'] = Faculty(code='FAC2')
+        self.app['faculties']['FAC3'] = Faculty(code='FAC3')
+        self.app['faculties']['FAC4'] = Faculty(code='FAC4')
+
+        role_manager1 = IPrincipalRoleManager(self.app['faculties']['FAC1'])
+        role_manager1.assignRoleToPrincipal('alfonsrole', 'alfons')
+        role_map1 = IPrincipalRoleMap(self.app['faculties']['FAC1'])
+        self.assertEqual(len(role_map1.getPrincipalsAndRoles()), 1)
+
+        role_manager2 = IPrincipalRoleManager(self.app['faculties']['FAC2'])
+        role_manager2.assignRoleToPrincipal('alfonsrole', 'alfons')
+        role_map2 = IPrincipalRoleMap(self.app['faculties']['FAC2'])
+        self.assertEqual(len(role_map2.getPrincipalsAndRoles()), 1)
+
+        role_manager3 = IPrincipalRoleManager(self.app['faculties']['FAC3'])
+        role_manager3.assignRoleToPrincipal('alfonsrole', 'alfons')
+        role_map3 = IPrincipalRoleMap(self.app['faculties']['FAC3'])
+        self.assertEqual(len(role_map3.getPrincipalsAndRoles()), 1)
+
+        role_manager4 = IPrincipalRoleManager(self.app['faculties']['FAC4'])
+        role_manager4.assignRoleToPrincipal('alfonsrole', 'alfons')
+        role_map4 = IPrincipalRoleMap(self.app['faculties']['FAC4'])
+        self.assertEqual(len(role_map4.getPrincipalsAndRoles()), 1)
+
+        local_role, user_name, setting = role_map2.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'alfons')
+        self.assertEqual(local_role, 'alfonsrole')
+
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_faculty_update, FACULTY_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        # Local roles have been removed in FAC1 due to deletion marker.
+        self.assertEqual(len(role_map1.getPrincipalsAndRoles()), 0)
+        # Old local roles have been removed and new roles have been added in FAC2.
+        self.assertEqual(len(role_map2.getPrincipalsAndRoles()), 1)
+        local_role, user_name, setting = role_map2.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'bob')
+        self.assertEqual(local_role, 'waeup.local.DepartmentManager')
+        # Local roles are not touched in FAC3 due to ignore marker.
+        self.assertEqual(len(role_map3.getPrincipalsAndRoles()), 1)
+        local_role, user_name, setting = role_map3.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'alfons')
+        self.assertEqual(local_role, 'alfonsrole')
+        # Local roles are not touched in FAC4 due to empty cell.
+        self.assertEqual(len(role_map4.getPrincipalsAndRoles()), 1)
+        local_role, user_name, setting = role_map4.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'alfons')
+        self.assertEqual(local_role, 'alfonsrole')
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+        
+    def test_import_update_logging(self):
+        self.app['faculties']['FAC2'] = Faculty(code='FAC2')
+        self.app['faculties']['FAC3'] = Faculty(code='FAC3')
+        self.app['faculties']['FAC4'] = Faculty(code='FAC4')
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_faculty_update, FACULTY_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Faculty Processor - sample_faculty_data_update - '
+            'FAC1 - updated: code=FAC1'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - Faculty Processor - sample_faculty_data_update - '
+            'FAC2 - updated: local_roles=bob|waeup.local.DepartmentManager, '
+            'code=FAC2'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - Faculty Processor - sample_faculty_data_update - '
+            'FAC3 - updated: code=FAC3'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - Faculty Processor - sample_faculty_data_update - '
+            'FAC4 - updated: code=FAC4'
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_upload_import_reupload(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        file = open(self.csv_file_faculty)
+        ctrl = self.browser.getControl(name='uploadfile:file')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file, filename='sample_faculty_data.csv')
+        self.browser.getControl('Upload').click()
+        self.browser.getLink('Process data').click()
+        self.assertTrue('Portal must be in maintenance mode for data import.'
+            in self.browser.contents)
+        self.browser.getLink("Switch").click()
+        self.assertTrue('Maintenance mode enabled' in self.browser.contents)
+        # Maintenance mode is now set.
+        self.assertEqual(
+            self.app['configuration'].maintmode_enabled_by, 'zope.mgr')
+        self.browser.getControl(name="select").click()
+        importerselect = self.browser.getControl(name='importer')
+        importerselect.getControl('Faculty Processor').selected = True
+        modeselect = self.browser.getControl(name='mode')
+        modeselect.getControl(value='create').selected = True
+        self.browser.getControl('Proceed to step 3').click()
+        self.assertTrue('Header fields OK' in self.browser.contents)
+        self.browser.getControl('Perform import').click()
+        self.assertTrue('Successfully processed 1 rows' in self.browser.contents)
+        self.assertTrue('Maintenance mode disabled' in self.browser.contents)
+        # Maintenance mode is really disabled.
+        self.assertEqual(
+            self.app['configuration'].maintmode_enabled_by, None)
+        # We import the same file a second time.
+        self.browser.open(self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        file = open(self.csv_file_faculty)
+        ctrl = self.browser.getControl(name='uploadfile:file')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(file, filename='sample_faculty_data.csv')
+        self.browser.getControl('Upload').click()
+        self.assertTrue(
+            'File with same name was uploaded earlier' in self.browser.contents)
+        return
+
+class TestDepartmentProcessor(UniversityProcessorSetup):
+
+    def setUp(self):
+        super(TestDepartmentProcessor, self).setUp()
+        self.proc = DepartmentProcessor()
+        self.site0 = dict()
+        self.site1 = dict(faculties=dict())
+        self.site2 = dict(faculties=dict(FAC=dict()))
+        self.site3 = dict(faculties=dict(FAC=dict(DPT='pseudo department')))
+        self.row = dict(code='DPT', faculty_code='FAC')
+
+        self.csv_file_department = os.path.join(self.workdir, 'sample_department_data.csv')
+        open(self.csv_file_department, 'wb').write(DEPARTMENT_SAMPLE_DATA)
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, DepartmentProcessor)
+        verifyObject(IBatchProcessor, self.proc)
+        return
+
+    def test_parents_exist(self):
+        # make sure we lookup parents correctly.
+        result0 = self.proc.parentsExist(self.row, self.site0)
+        result1 = self.proc.parentsExist(self.row, self.site1)
+        result2 = self.proc.parentsExist(self.row, self.site2)
+        result3 = self.proc.parentsExist(self.row, self.site3)
+        self.assertTrue(result0 is False)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is True)
+        self.assertTrue(result3 is True)
+        return
+
+    def test_entry_exists(self):
+        # make sure we lookup entries correctly.
+        result0 = self.proc.entryExists(self.row, dict())
+        result1 = self.proc.entryExists(self.row, self.site1)
+        result2 = self.proc.entryExists(self.row, self.site2)
+        result3 = self.proc.entryExists(self.row, self.site3)
+        self.assertTrue(result0 is False)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is True)
+        return
+
+    def test_get_entry(self):
+        # we can get a dept. if it exists
+        result1 = self.proc.getEntry(self.row, self.site2)
+        result2 = self.proc.getEntry(self.row, self.site3)
+        self.assertTrue(result1 is None)
+        self.assertEqual(result2, 'pseudo department')
+        return
+
+    def test_del_entry(self):
+        # we can delete departments
+        self.proc.delEntry(self.row, self.site3)
+        self.assertTrue('DPT' not in self.site3['faculties']['FAC'].keys())
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_department, DEPARTMENT_HEADER_FIELDS)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,6)
+        self.assertEqual(
+            content,
+            'faculty_code,code,local_roles,--ERRORS--\r\n'
+            'FAC1,DEP2,"[{\'user_name\':\'alice\',\'local_role\':\'waeup.local.DepartmentManager\'}]",'
+            'local_roles: alice does not exist\r\n'
+            'FAC1,DEP2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.Boss\'}]",'
+            'local_roles: waeup.local.Boss not allowed\r\n'
+            'FAC1,DEP2,[(\'anything\')],local_roles: no dicts\r\n'
+            'FAC1,DEP2,[,local_roles: Error\r\n'
+            'FAC1,DEP2,"[{\'user\':\'anne\',\'local\':\'waeup.local.DepartmentManager\'}]",'
+            'local_roles: user_name or local_role missing\r\n'
+            'FAC11,DEP2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.DepartmentManager\'}]",'
+            'Not all parents do exist yet.\r\n'
+            )
+        # Anne got a local role in department DEP2.
+        dep = self.app['faculties']['FAC1']['DEP2']
+        role_map = IPrincipalRoleMap(dep)
+        local_role, user_name, setting = role_map.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'anne')
+        self.assertEqual(local_role, 'waeup.local.DepartmentManager')
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+class CourseProcessorTests(UniversityProcessorSetup):
+
+    def setUp(self):
+        super(CourseProcessorTests, self).setUp()
+        self.proc = CourseProcessor()
+        self.row1 = dict(department_code='DEP2', faculty_code='FAC1', code="CRS1")
+        self.row2 = dict(department_code='DEP1', faculty_code='FAC2', code="CRS1")
+        self.row3 = dict(department_code='DEP1', faculty_code='FAC1', code="CRS2")
+        self.row4 = dict(department_code='DEP1', faculty_code='FAC1', code="CRS1")
+
+        self.csv_file_course = os.path.join(
+            self.workdir, 'sample_course_data.csv')
+        open(self.csv_file_course, 'wb').write(COURSE_SAMPLE_DATA)
+        self.csv_file_course_update = os.path.join(
+            self.workdir, 'sample_course_data_update.csv')
+        open(self.csv_file_course_update, 'wb').write(COURSE_SAMPLE_DATA_UPDATE)
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, CourseProcessor)
+        verifyObject(IBatchProcessor, self.proc)
+        return
+
+    def test_parents_exist(self):
+        # make sure we lookup parents correctly
+        result1 = self.proc.parentsExist(self.row1, self.app)
+        result2 = self.proc.parentsExist(self.row2, self.app)
+        result3 = self.proc.parentsExist(self.row3, self.app)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is True)
+        return
+
+    def test_entry_exists(self):
+        # make sure we find an entry if it exists
+        result1 = self.proc.entryExists(self.row1, self.app)
+        result2 = self.proc.entryExists(self.row2, self.app)
+        result3 = self.proc.entryExists(self.row3, self.app)
+        result4 = self.proc.entryExists(self.row4, self.app)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is False)
+        self.assertTrue(result4 is True)
+        return
+
+    def test_get_entry(self):
+        # make sure we can get an entry if it exists
+        result1 = self.proc.getEntry(self.row1, self.app)
+        result2 = self.proc.getEntry(self.row4, self.app)
+        self.assertTrue(result1 is None)
+        self.assertTrue(result2 is self.course)
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRS1' in self.app['faculties']['FAC1']['DEP1'].courses.keys())
+        self.proc.delEntry(self.row4, self.app)
+        self.assertTrue('CRS1' not in self.app['faculties']['FAC1']['DEP1'].courses.keys())
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_course, COURSE_HEADER_FIELDS)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,6)
+        self.assertEqual(
+            content,
+            'faculty_code,department_code,code,local_roles,--ERRORS--\r\n'
+            'FAC1,DEP1,CRS2,"[{\'user_name\':\'alice\',\'local_role\':\'waeup.local.Lecturer\'}]",'
+            'local_roles: alice does not exist\r\n'
+            'FAC1,DEP1,CRS2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.Boss\'}]",'
+            'local_roles: waeup.local.Boss not allowed\r\n'
+            'FAC1,DEP1,CRS2,[(\'anything\')],local_roles: no dicts\r\n'
+            'FAC1,DEP1,CRS2,[,local_roles: Error\r\n'
+            'FAC1,DEP1,CRS2,"[{\'user\':\'anne\',\'local\':\'waeup.local.Lecturer\'}]",'
+            'local_roles: user_name or local_role missing\r\n'
+            'FAC11,DEP2,CRS2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.Lecturer\'}]",'
+            'Not all parents do exist yet.\r\n'
+            )
+        # Anne got a local role in course CRS2.
+        dep = self.app['faculties']['FAC1']['DEP1'].courses['CRS2']
+        role_map = IPrincipalRoleMap(dep)
+        local_role, user_name, setting = role_map.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'anne')
+        self.assertEqual(local_role, 'waeup.local.Lecturer')
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_course, COURSE_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_course_update, COURSE_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        self.assertEqual(
+            self.app['faculties']['FAC1']['DEP1'].courses['CRS2'].title,
+            'New Title')
+        shutil.rmtree(os.path.dirname(fin_file))
+        return
+
+
+class CertificateProcessorTests(UniversityProcessorSetup):
+
+    def setUp(self):
+        super(CertificateProcessorTests, self).setUp()
+        self.proc = CertificateProcessor()
+        self.row1 = dict(department_code='DEP2', faculty_code='FAC1', code="CRT1")
+        self.row2 = dict(department_code='DEP1', faculty_code='FAC2', code="CRT1")
+        self.row3 = dict(department_code='DEP1', faculty_code='FAC1', code="CRT2")
+        self.row4 = dict(department_code='DEP1', faculty_code='FAC1', code="CRT1")
+
+        self.csv_file_certificate = os.path.join(self.workdir, 'sample_certificate_data.csv')
+        open(self.csv_file_certificate, 'wb').write(CERTIFICATE_SAMPLE_DATA)
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, CourseProcessor)
+        verifyObject(IBatchProcessor, self.proc)
+        return
+
+    def test_parents_exist(self):
+        # make sure we lookup parents correctly
+        result1 = self.proc.parentsExist(self.row1, self.app)
+        result2 = self.proc.parentsExist(self.row2, self.app)
+        result3 = self.proc.parentsExist(self.row3, self.app)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is True)
+        return
+
+    def test_entry_exists(self):
+        # make sure we find an entry if it exists
+        result1 = self.proc.entryExists(self.row1, self.app)
+        result2 = self.proc.entryExists(self.row2, self.app)
+        result3 = self.proc.entryExists(self.row3, self.app)
+        result4 = self.proc.entryExists(self.row4, self.app)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is False)
+        self.assertTrue(result4 is True)
+        return
+
+    def test_get_entry(self):
+        # make sure we can get an entry if it exists
+        result1 = self.proc.getEntry(self.row1, self.app)
+        result2 = self.proc.getEntry(self.row4, self.app)
+        self.assertTrue(result1 is None)
+        self.assertTrue(result2 is self.certificate)
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRT1' in self.app['faculties']['FAC1']['DEP1'].certificates.keys())
+        self.proc.delEntry(self.row4, self.app)
+        self.assertTrue('CRT1' not in self.app['faculties']['FAC1']['DEP1'].certificates.keys())
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_certificate, CERTIFICATE_HEADER_FIELDS)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,6)
+        self.assertEqual(
+            content,
+            'faculty_code,department_code,code,local_roles,--ERRORS--\r\n'
+            'FAC1,DEP1,CRT2,"[{\'user_name\':\'alice\',\'local_role\':\'waeup.local.CourseAdviser100\'}]",'
+            'local_roles: alice does not exist\r\n'
+            'FAC1,DEP1,CRT2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.Boss\'}]",'
+            'local_roles: waeup.local.Boss not allowed\r\n'
+            'FAC1,DEP1,CRT2,[(\'anything\')],local_roles: no dicts\r\n'
+            'FAC1,DEP1,CRT2,[,local_roles: Error\r\n'
+            'FAC1,DEP1,CRT2,"[{\'user\':\'anne\',\'local\':\'waeup.local.CourseAdviser100\'}]",'
+            'local_roles: user_name or local_role missing\r\n'
+            'FAC11,DEP2,CRT2,"[{\'user_name\':\'anne\',\'local_role\':\'waeup.local.CourseAdviser100\'}]",'
+            'Not all parents do exist yet.\r\n'
+            )
+        # Anne got a local role in certificate CRT2.
+        dep = self.app['faculties']['FAC1']['DEP1'].certificates['CRT2']
+        role_map = IPrincipalRoleMap(dep)
+        local_role, user_name, setting = role_map.getPrincipalsAndRoles()[0]
+        self.assertEqual(user_name, 'anne')
+        self.assertEqual(local_role, 'waeup.local.CourseAdviser100')
+        shutil.rmtree(os.path.dirname(fin_file))
+        
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - Certificate Processor - '
+            'sample_certificate_data - CRT2 - '
+            'updated: local_roles=anne|waeup.local.CourseAdviser100, code=CRT2'
+            in logcontent)        
+        
+        return
+
+
+class CertCourseProcessorTests(UniversityProcessorSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CertCourseProcessorTests, self).setUp()
+        self.proc = CertificateCourseProcessor()
+        self.certificate.addCertCourse(self.course)
+        self.row1 = dict(
+            department_code='DEP1',
+            faculty_code='FAC1',
+            certificate_code='CRT1',
+            course=self.course, level='100')
+        self.row2 = dict(
+            department_code='DEP1',
+            faculty_code='FAC1',
+            certificate_code='CRT2',
+            course=self.course, level='100')
+        self.csv_file_certificatecourse = os.path.join(
+            self.workdir, 'sample_certificatecourse_data.csv')
+        open(self.csv_file_certificatecourse, 'wb').write(CERTIFICATECOURSE_SAMPLE_DATA)
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, CertificateCourseProcessor)
+        verifyObject(IBatchProcessor, self.proc)
+        return
+
+    def test_parents_exist(self):
+        # make sure we can find all certificate parents
+        result1 = self.proc.parentsExist(self.row1, self.app)
+        self.assertTrue(result1)
+        result2 = self.proc.parentsExist(self.row2, self.app)
+        self.assertFalse(result2)
+        return
+
+    def test_entry_exists(self):
+        # make sure we find an entry if it exists
+        result1 = self.proc.entryExists(self.row1, self.app)
+        self.assertTrue(result1)
+        result2 = self.proc.entryExists(self.row2, self.app)
+        self.assertFalse(result2)
+        return
+
+    def test_get_entry(self):
+        # make sure we can get an entry if it exists
+        result1 = self.proc.getEntry(self.row1, self.app)
+        self.assertTrue(result1 is self.certificate['CRS1_100'])
+        result2 = self.proc.getEntry(self.row2, self.app)
+        self.assertFalse(result2 is self.certificate['CRS1_100'])
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRS1_100' in self.certificate.keys())
+        self.proc.delEntry(self.row1, self.app)
+        self.assertTrue('CRS1_100' not in self.certificate.keys())
+        return
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.proc.doImport(
+            self.csv_file_certificatecourse, CERTIFICATECOURSE_HEADER_FIELDS)
+        content = open(fail_file).read()
+        self.assertEqual(num_warns,2) # the last two
+        self.assertEqual(
+            content,
+            'mandatory,level,faculty_code,course,department_code,'
+            'certificate_code,--ERRORS--\r\n'
+            '<IGNORE>,100,FAC1,CRS1,'
+            'DEP1,CRT1,This object already exists.\r\n'
+            '<IGNORE>,100,FAC1,CRS1,DEP1,CRT2,Not all parents do exist yet.\r\n'
+            )
+        logcontent = open(self.logfile).read()
+        # Logging message from updateEntry
+        self.assertTrue(
+            'INFO - system - CertificateCourse Processor - '
+            'sample_certificatecourse_data - CRT1 - updated: '
+            'mandatory=True, level=200, course=CRS1\n'
+            in logcontent)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_certificatescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_certificatescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_certificatescontainer.py	(revision 16045)
@@ -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
+##
+"""
+Tests for certificate containers and related components.
+"""
+import shutil
+import tempfile
+import unittest
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import DuplicationError
+from waeup.kofa.testing import (
+    FunctionalLayer, doctestsuite_for_module, FunctionalTestCase)
+from waeup.kofa.university.certificate import Certificate
+from waeup.kofa.university.certificatescontainer import CertificatesContainer
+from waeup.kofa.university.interfaces import ICertificatesContainer
+
+
+class CertificatesContainerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CertificatesContainerTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        setSite(self.app)
+
+    def tearDown(self):
+        super(CertificatesContainerTests, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+    def test_create(self):
+        # we can create instances
+        container = CertificatesContainer()
+        result = isinstance(container, CertificatesContainer)
+        assert result is True
+
+    def test_provides_icertificatescontainer(self):
+        # instances tell they provide their main interface
+        container = CertificatesContainer()
+        result = ICertificatesContainer.providedBy(container)
+        assert result is True
+
+    def test_interfaces(self):
+        # class and instances comply with their main interface
+        container = CertificatesContainer()
+        self.assertTrue(verifyClass(ICertificatesContainer, CertificatesContainer))
+        self.assertTrue(verifyObject(ICertificatesContainer, container))
+
+    def test_add_certificate_non_icertificate(self):
+        # we cannot add certificates that do not implement ICertificate.
+        container = CertificatesContainer()
+        self.assertRaises(
+            TypeError, container.addCertificate, 'some_string')
+
+    def test_add_certificate_no_duplicate_entry(self):
+        # we cannot add certificates that are registered already.
+        container = CertificatesContainer()
+        # We must place this container in the ZODB to make catalogs work.
+        self.app['certs'] = container
+        cert1 = Certificate(code="CERT1")
+        cert2 = Certificate(code="CERT1")
+        container.addCertificate(cert1)
+        self.assertRaises(
+            DuplicationError,
+            container.addCertificate, cert2)
+
+    def test_setitem_duplicate_entry(self):
+        # we cannot add certificates whose code exists already in catalog
+        container1 = CertificatesContainer()
+        container2 = CertificatesContainer()
+        self.app['certs1'] = container1 # enable catalogs
+        self.app['certs2'] = container2
+        cert1 = Certificate(code="CERT1")
+        cert2 = Certificate(code="CERT1")
+        self.app['certs1']['CERT1'] = cert1
+        self.assertRaises(
+            DuplicationError,
+            self.app['certs2'].__setitem__, 'CERT1', cert2)
+        assert len(container2) == 0
+
+    def test_setitem_name_unequal_code(self):
+        # we cannot add certificates whose code != key
+        container = CertificatesContainer()
+        self.app['certs'] = container # enable catalogs
+        cert1 = Certificate(code="CERT1")
+        self.assertRaises(
+            ValueError,
+            container.__setitem__, 'OtherKey', cert1)
+        assert len(container) == 0
+
+    def test_clear(self):
+        # clear() really empties the container.
+        container = CertificatesContainer()
+        self.app['certs'] = container # enable catalogs
+        cert1 = Certificate(code="CERT1")
+        cert2 = Certificate(code="CERT2")
+        container['CERT1'] = cert1
+        container['CERT2'] = cert2
+        container.clear()
+        assert len(container) == 0
+
+
+def test_suite():
+    suite = unittest.TestSuite((
+        unittest.makeSuite(CertificatesContainerTests),
+        doctestsuite_for_module(
+                'waeup.kofa.university.certificatescontainer'),
+        ))
+    return suite
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_coursescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_coursescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_coursescontainer.py	(revision 16045)
@@ -0,0 +1,39 @@
+## $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 course containers
+
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.university.interfaces import ICoursesContainer
+from waeup.kofa.university import CoursesContainer, Course
+
+class CoursesContainerTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        container = CoursesContainer()
+        self.assertTrue(verifyClass(ICoursesContainer, CoursesContainer))
+        self.assertTrue(verifyObject(ICoursesContainer, container))
+
+    def test_setitem_name_ne_code(self):
+        # If we add a course under a wrong name that will give an error
+        container = CoursesContainer()
+        course = Course(code='MYCOURSE')
+        self.assertRaises(
+            ValueError,
+            container.__setitem__, 'NOTMYCOURSE', course)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_export.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_export.py	(revision 16045)
@@ -0,0 +1,587 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component import queryUtility
+from zope.interface.verify import verifyObject, verifyClass
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.testing import KofaUnitTestLayer, FunctionalLayer
+from waeup.kofa.university import (
+    FacultiesContainer, Faculty, Department, Course, Certificate,
+    )
+from waeup.kofa.university.export import (
+    FacultyExporter, DepartmentExporter, CourseExporter,
+    CertificateExporter, CertificateCourseExporter,
+    )
+
+class FacultyExporterTest(unittest.TestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = FacultyExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, FacultyExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a faculty exporter as utility
+        result = queryUtility(ICSVExporter, name="faculties")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can export a set of faculties
+        fac = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        exporter = FacultyExporter()
+        exporter.export([fac], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2\r\n'
+            'F1,Faculty of Cheese,faculty,[],,\r\n'
+            )
+        return
+
+    def test_export_to_string(self):
+        # we can export a set of faculties to a string.
+        fac = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        exporter = FacultyExporter()
+        result = exporter.export([fac], filepath=None)
+        self.assertEqual(
+            result,
+            'code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2\r\n'
+            'F1,Faculty of Cheese,faculty,[],,\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can export all faculties in a site
+        container = FacultiesContainer()
+        site = {'faculties':container}
+        fac1 = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        fac2 = Faculty('Centre of Onion', 'centre', 'F2')
+        container.addFaculty(fac1)
+        container.addFaculty(fac2)
+        exporter = FacultyExporter()
+        exporter.export_all(site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2\r\n'
+            'F1,Faculty of Cheese,faculty,[],,\r\n'
+            'F2,Centre of Onion,centre,[],,\r\n'
+            )
+        return
+
+    def test_export_all_to_string(self):
+        # we can export all faculties in a site to a string
+        container = FacultiesContainer()
+        site = {'faculties':container}
+        fac1 = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        fac2 = Faculty('Centre of Onion', 'centre', 'F2')
+        container.addFaculty(fac1)
+        container.addFaculty(fac2)
+        exporter = FacultyExporter()
+        result = exporter.export_all(site, filepath=None)
+        self.assertEqual(
+            result,
+            'code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2\r\n'
+            'F1,Faculty of Cheese,faculty,[],,\r\n'
+            'F2,Centre of Onion,centre,[],,\r\n'
+            )
+        return
+
+class DepartmentExporterTest(unittest.TestCase):
+    # Tests for DepartmentExporter
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        # create some departments in a fake site
+        container = FacultiesContainer()
+        self.site = {'faculties':container}
+        self.fac1 = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        self.fac2 = Faculty('Centre of Onion', 'centre', 'F2')
+        container.addFaculty(self.fac1)
+        container.addFaculty(self.fac2)
+        self.dept1 = Department(
+            'Department of Cheddar', 'department', 'D1', 'HoCheese')
+        self.dept2 = Department('Institue of Gouda', 'institute', 'D2')
+        self.dept3 = Department('Department of Rings', 'department', 'D3')
+        self.fac1.addDepartment(self.dept1)
+        self.fac1.addDepartment(self.dept2)
+        self.fac2.addDepartment(self.dept3)
+        role_manager = IPrincipalRoleManager(self.dept1)
+        role_manager.assignRoleToPrincipal(u'bobsrole', u'bob')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = DepartmentExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, DepartmentExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a department exporter as utility
+        result = queryUtility(ICSVExporter, name="departments")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can export an iterable of departments
+        exporter = DepartmentExporter()
+        exporter.export([self.dept1], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2,officer_3,officer_4\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]",'
+            'HoCheese,,,\r\n'
+            )
+        return
+
+    def test_export_to_string(self):
+        # we can export an iterable of departments to a string.
+        exporter = DepartmentExporter()
+        result = exporter.export([self.dept1, self.dept2], filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2,officer_3,officer_4\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]",'
+            'HoCheese,,,\r\n'
+            'D2,F1,Institue of Gouda,institute,[],,,,\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can export all depts in a site
+        exporter = DepartmentExporter()
+        exporter.export_all(self.site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2,officer_3,officer_4\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]",'
+            'HoCheese,,,\r\n'
+            'D2,F1,Institue of Gouda,institute,[],,,,\r\n'
+            'D3,F2,Department of Rings,department,[],,,,\r\n'
+            )
+        return
+
+    def test_export_all_to_string(self):
+        # we can export all depts in a site to a string
+        exporter = DepartmentExporter()
+        result = exporter.export_all(self.site, filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,title,title_prefix,users_with_local_roles,'
+            'officer_1,officer_2,officer_3,officer_4\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]",'
+            'HoCheese,,,\r\n'
+            'D2,F1,Institue of Gouda,institute,[],,,,\r\n'
+            'D3,F2,Department of Rings,department,[],,,,\r\n'
+            )
+        return
+
+class CourseExporterTest(unittest.TestCase):
+    # Tests for CourseExporter
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        # create some departments and courses in a fake site
+        container = FacultiesContainer()
+        self.site = {'faculties':container}
+        self.fac = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        container.addFaculty(self.fac)
+        self.dept1 = Department('Department of Cheddar', 'department', 'D1')
+        self.dept2 = Department('Institue of Gouda', 'institute', 'D2')
+        self.fac.addDepartment(self.dept1)
+        self.fac.addDepartment(self.dept2)
+        self.course1 = Course('Cheese Basics', 'C1')
+        self.course2 = Course('Advanced Cheese Making', 'C2')
+        self.course3 = Course('Selling Cheese', 'C3')
+        self.dept1.courses.addCourse(self.course1)
+        self.dept1.courses.addCourse(self.course2)
+        self.dept2.courses.addCourse(self.course3)
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CourseExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, CourseExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a course exporter as utility
+        result = queryUtility(ICSVExporter, name="courses")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can export an iterable of courses
+        exporter = CourseExporter()
+        exporter.export([self.course1], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,credits,'
+            'passmark,semester,users_with_local_roles,former_course,'
+            'results_validated_by,results_validation_date,'
+            'results_validation_session\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1,[],0,,,\r\n'
+            )
+        return
+
+    def test_export_to_string(self):
+        # we can export an iterable of courses to a string.
+        exporter = CourseExporter()
+        result = exporter.export([self.course1, self.course2], filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,credits,passmark,'
+            'semester,users_with_local_roles,former_course,'
+            'results_validated_by,results_validation_date,'
+            'results_validation_session\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1,[],0,,,\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1,[],0,,,\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can export all courses in a site
+        exporter = CourseExporter()
+        exporter.export_all(self.site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,credits,passmark,'
+            'semester,users_with_local_roles,former_course,'
+            'results_validated_by,results_validation_date,'
+            'results_validation_session\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1,[],0,,,\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1,[],0,,,\r\n'
+            'C3,F1,D2,Selling Cheese,0,40,1,[],0,,,\r\n'
+            )
+        return
+
+    def test_export_all_to_string(self):
+        # we can export all courses in a site to a string
+        exporter = CourseExporter()
+        result = exporter.export_all(self.site, filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,credits,passmark,'
+            'semester,users_with_local_roles,former_course,'
+            'results_validated_by,results_validation_date,'
+            'results_validation_session\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1,[],0,,,\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1,[],0,,,\r\n'
+            'C3,F1,D2,Selling Cheese,0,40,1,[],0,,,\r\n'
+            )
+        return
+
+class CertificateExporterTest(unittest.TestCase):
+    # Tests for CertificateExporter
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        # create some departments and courses in a fake site
+        container = FacultiesContainer()
+        self.site = {'faculties':container}
+        self.fac = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        container.addFaculty(self.fac)
+        self.dept1 = Department('Department of Cheddar', 'department', 'D1')
+        self.dept2 = Department('Institue of Gouda', 'institute', 'D2')
+        self.fac.addDepartment(self.dept1)
+        self.fac.addDepartment(self.dept2)
+        self.course1 = Course('Cheese Basics', 'C1')
+        self.course2 = Course('Advanced Cheese Making', 'C2')
+        self.course3 = Course('Selling Cheese', 'C3')
+        self.dept1.courses.addCourse(self.course1)
+        self.dept1.courses.addCourse(self.course2)
+        self.dept2.courses.addCourse(self.course3)
+        self.cert1 = Certificate(
+            'CERT1', 'Master of Cheese', study_mode=u'ct_ft', start_level=100,
+            end_level=300, application_category='basic')
+        self.cert2 = Certificate(
+            'CERT2', 'Master of Cheddar', study_mode='ct_ft', start_level=400,
+            end_level=700, application_category='cest')
+        self.cert3 = Certificate(
+            'CERT3', 'Cert. of Rubbish', study_mode='dp_pt', start_level=100,
+            end_level=200, application_category='no')
+        self.dept1.certificates.addCertificate(self.cert1)
+        self.dept1.certificates.addCertificate(self.cert2)
+        self.dept2.certificates.addCertificate(self.cert3)
+        role_manager = IPrincipalRoleManager(self.cert1)
+        role_manager.assignRoleToPrincipal(u'bobsrole', u'bob')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CertificateExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, CertificateExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a certificate exporter as utility
+        result = queryUtility(ICSVExporter, name="certificates")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can export an iterable of certificates
+        exporter = CertificateExporter()
+        exporter.export([self.cert1], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,study_mode,'
+            'degree,start_level,'
+            'end_level,application_category,ratio,school_fee_1,'
+            'school_fee_2,school_fee_3,school_fee_4,'
+            'custom_textline_1,custom_textline_2,'
+            'custom_float_1,custom_float_2,'
+            'users_with_local_roles\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,,100,300,basic,,,,,,,,,,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\r\n'
+            )
+        return
+
+    def test_export_to_string(self):
+        # we can export an iterable of certificates to a string.
+        exporter = CertificateExporter()
+        result = exporter.export([self.cert1, self.cert2], filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,study_mode,'
+            'degree,start_level,'
+            'end_level,application_category,ratio,school_fee_1,'
+            'school_fee_2,school_fee_3,school_fee_4,'
+            'custom_textline_1,custom_textline_2,'
+            'custom_float_1,custom_float_2,'
+            'users_with_local_roles\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,,100,300,basic,,,,,,,,,,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\r\n'
+            'CERT2,F1,D1,Master of Cheddar,ct_ft,,400,700,cest,,,,,,,,,,[]\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can export all certificates in a site
+        exporter = CertificateExporter()
+        exporter.export_all(self.site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,study_mode,'
+            'degree,start_level,'
+            'end_level,application_category,ratio,'
+            'school_fee_1,school_fee_2,school_fee_3,school_fee_4,'
+            'custom_textline_1,custom_textline_2,'
+            'custom_float_1,custom_float_2,'
+            'users_with_local_roles\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,,100,300,basic,,,,,,,,,,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\r\n'
+            'CERT2,F1,D1,Master of Cheddar,ct_ft,,400,700,cest,,,,,,,,,,[]\r\n'
+            'CERT3,F1,D2,Cert. of Rubbish,dp_pt,,100,200,no,,,,,,,,,,[]\r\n'
+            )
+        return
+
+    def test_export_all_to_string(self):
+        # we can export all certificates in a site to a string
+        exporter = CertificateExporter()
+        result = exporter.export_all(self.site, filepath=None)
+        self.assertEqual(
+            result,
+            'code,faculty_code,department_code,title,study_mode,'
+            'degree,start_level,'
+            'end_level,application_category,ratio,'
+            'school_fee_1,school_fee_2,school_fee_3,school_fee_4,'
+            'custom_textline_1,custom_textline_2,'
+            'custom_float_1,custom_float_2,'
+            'users_with_local_roles\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,,100,300,basic,,,,,,,,,,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\r\n'
+            'CERT2,F1,D1,Master of Cheddar,ct_ft,,400,700,cest,,,,,,,,,,[]\r\n'
+            'CERT3,F1,D2,Cert. of Rubbish,dp_pt,,100,200,no,,,,,,,,,,[]\r\n'
+            )
+        return
+
+class CertificateCourseExporterTest(unittest.TestCase):
+    # Tests for CertificateCourseExporter
+
+    layer = KofaUnitTestLayer
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.outfile = os.path.join(self.workdir, 'myoutput.csv')
+        # create some departments and courses in a fake site
+        container = FacultiesContainer()
+        self.site = {'faculties':container}
+        self.fac = Faculty('Faculty of Cheese', 'faculty', 'F1')
+        container.addFaculty(self.fac)
+        self.dept1 = Department('Department of Cheddar', 'department', 'D1')
+        self.dept2 = Department('Institue of Gouda', 'institute', 'D2')
+        self.fac.addDepartment(self.dept1)
+        self.fac.addDepartment(self.dept2)
+        self.course1 = Course('Cheese Basics', 'C1')
+        self.course2 = Course('Advanced Cheese Making', 'C2')
+        self.course3 = Course('Selling Cheese', 'C3')
+        self.dept1.courses.addCourse(self.course1)
+        self.dept1.courses.addCourse(self.course2)
+        self.dept2.courses.addCourse(self.course3)
+        self.cert1 = Certificate(
+            'CERT1', 'Master of Cheese', study_mode=u'ct_ft', start_level=100,
+            end_level=300, application_category='basic')
+        self.cert2 = Certificate(
+            'CERT2', 'Master of Cheddar', study_mode='ct_ft', start_level=400,
+            end_level=700, application_category='cest')
+        self.cert3 = Certificate(
+            'CERT3', 'Cert. of Rubbish', study_mode='dp_pt', start_level=100,
+            end_level=200, application_category='no')
+        self.dept1.certificates.addCertificate(self.cert1)
+        self.dept1.certificates.addCertificate(self.cert2)
+        self.dept2.certificates.addCertificate(self.cert3)
+        self.cert1.addCertCourse(self.course1, 100, True)
+        self.cert1.addCertCourse(self.course2, 400, False)
+        self.cert3.addCertCourse(self.course3, 100, False)
+        self.certcourse1 = self.cert1['C1_100']
+        self.certcourse2 = self.cert1['C2_400']
+        self.certcourse3 = self.cert3['C3_100']
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = CertificateCourseExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, CertificateCourseExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a certificate exporter as utility
+        result = queryUtility(ICSVExporter, name="certificate_courses")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        # we can export an iterable of certificates
+        exporter = CertificateCourseExporter()
+        exporter.export([self.certcourse1], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'course,faculty_code,department_code,certificate_code,'
+            'level,mandatory,course_category\r\n'
+            'C1,F1,D1,CERT1,100,1,\r\n'
+            )
+        return
+
+    def test_export_to_string(self):
+        # we can export an iterable of certificates to a string.
+        exporter = CertificateCourseExporter()
+        result = exporter.export(
+            [self.certcourse1, self.certcourse2], filepath=None)
+        self.assertEqual(
+            result,
+            'course,faculty_code,department_code,certificate_code,'
+            'level,mandatory,course_category\r\n'
+            'C1,F1,D1,CERT1,100,1,\r\n'
+            'C2,F1,D1,CERT1,400,0,\r\n'
+            )
+        return
+
+    def test_export_all(self):
+        # we can export all certificates in a site
+        exporter = CertificateCourseExporter()
+        exporter.export_all(self.site, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'course,faculty_code,department_code,certificate_code,'
+            'level,mandatory,course_category\r\n'
+            'C1,F1,D1,CERT1,100,1,\r\n'
+            'C2,F1,D1,CERT1,400,0,\r\n'
+            'C3,F1,D2,CERT3,100,0,\r\n'
+            )
+        return
+
+    def test_export_all_to_string(self):
+        # we can export all certificates in a site to a string
+        exporter = CertificateCourseExporter()
+        result = exporter.export_all(self.site, filepath=None)
+        self.assertEqual(
+            result,
+            'course,faculty_code,department_code,certificate_code,'
+            'level,mandatory,course_category\r\n'
+            'C1,F1,D1,CERT1,100,1,\r\n'
+            'C2,F1,D1,CERT1,400,0,\r\n'
+            'C3,F1,D2,CERT3,100,0,\r\n'
+            )
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_facultiescontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_facultiescontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_facultiescontainer.py	(revision 16045)
@@ -0,0 +1,114 @@
+## $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
+##
+
+# Tests for FacultiesContainer.
+import logging
+import unittest
+from StringIO import StringIO
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.university.interfaces import IFacultiesContainer
+from waeup.kofa.university import FacultiesContainer
+from waeup.kofa.university.facultiescontainer import AcademicsPlugin
+
+class FakeLogger(object):
+
+    _stream = None
+
+    def get_logger(self):
+        logger = logging.getLogger('test')
+        logger.propagate = False
+        self._stream = StringIO()
+        self.handler = logging.StreamHandler(self._stream)
+        logger.setLevel(logging.DEBUG)
+        logger.addHandler(self.handler)
+        return logger
+
+    def close(self, logger):
+        # Remove handler from logger
+        handler = self.handler
+        if handler not in logger.handlers:
+            return
+        del logger.handlers[logger.handlers.index(handler)]
+        return
+
+    def get_messages(self):
+        self._stream.seek(0)
+        result = self._stream.read()
+        self._stream.seek(0, 2) # Seek to end of stream
+        return result
+
+class FacultiesContainerTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        container = FacultiesContainer()
+        self.assertTrue(verifyClass(IFacultiesContainer, FacultiesContainer))
+        self.assertTrue(verifyObject(IFacultiesContainer, container))
+
+class AcademicsPluginTests(unittest.TestCase):
+
+    def setUp(self):
+        self._logger_factory = FakeLogger()
+        self.logger = self._logger_factory.get_logger()
+
+    def tearDown(self):
+        self._logger_factory.close(self.logger)
+
+    def test_setup(self):
+        site = dict()
+        logger = self.logger
+        plugin = AcademicsPlugin()
+        plugin.setup(site, 'testsite', logger)
+        self.assertTrue('faculties' in site.keys())
+        self.assertTrue(isinstance(site['faculties'], FacultiesContainer))
+        self.assertEqual(
+            self._logger_factory.get_messages(),
+            'Container for faculties created\n'
+            )
+
+    def test_setup_already_set_up(self):
+        site = dict(faculties=object())
+        logger = self.logger
+        plugin = AcademicsPlugin()
+        plugin.setup(site, 'testsite', logger)
+        self.assertTrue('faculties' in site.keys())
+        self.assertEqual(
+            self._logger_factory.get_messages(),
+            'Could not create container for faculties in Kofa.\n'
+            )
+
+    def test_update(self):
+        site = dict()
+        logger = self.logger
+        plugin = AcademicsPlugin()
+        plugin.update(site, 'testsite', logger)
+        self.assertTrue('faculties' in site.keys())
+        self.assertTrue(isinstance(site['faculties'], FacultiesContainer))
+        self.assertEqual(
+            self._logger_factory.get_messages(),
+            'Container for faculties created\n'
+            )
+
+    def test_update_already_set_up(self):
+        site = dict(faculties=object())
+        logger = self.logger
+        plugin = AcademicsPlugin()
+        plugin.update(site, 'testsite', logger)
+        self.assertTrue('faculties' in site.keys())
+        self.assertEqual(
+            self._logger_factory.get_messages(), '')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_faculty.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_faculty.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_faculty.py	(revision 16045)
@@ -0,0 +1,37 @@
+## $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 faculties
+import tempfile
+import shutil
+import unittest
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.kofa.university import Faculty, Department
+from waeup.kofa.university.interfaces import IFaculty, IDepartment
+
+class FacultyAndDepartmentTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # Make sure we implement the promised interfaces
+        fac = Faculty()
+        verifyClass(IFaculty, Faculty)
+        verifyObject(IFaculty, fac)
+        dep = Department()
+        verifyClass(IDepartment, Department)
+        verifyObject(IDepartment, dep)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_move.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_move.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_move.py	(revision 16045)
@@ -0,0 +1,164 @@
+## $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
+##
+"""
+Tests for moving objects in faculties.
+"""
+import os
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+
+class MoveObjectsInFaculties(StudentsFullSetup):
+
+    def test_move_department(self):
+        self.app['faculties']['fac2'] = Faculty(code=u'fac2')
+        self.assertEqual(
+            self.app['faculties']['fac1']['dep1'].certificates['CERT1'],
+            self.certificate)
+        department = self.app['faculties']['fac1']['dep1']
+        faculty = self.app['faculties']['fac1']
+
+        # We move the depart using the UtilityView
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/faculties/fac1/dep1/move_department?fac=fac2&dep=newdep')
+
+        # New department is created and old department is removed
+        self.assertEqual(
+            [key for key in self.app['faculties']['fac2']], ['newdep'])
+
+        # All subobjects have been moved too
+        self.assertEqual(
+            self.app['faculties']['fac2']['newdep'].courses['COURSE1'],
+            self.course)
+        self.assertEqual(
+            self.app['faculties']['fac2']['newdep'].certificates['CERT1'],
+            self.certificate)
+        self.assertEqual(
+            [key for key in self.app['faculties']['fac2'][
+                'newdep'].certificates['CERT1'].keys()],
+            [u'COURSE1_100'])
+
+        # We can still find the certificate in the catalog
+        cat = queryUtility(ICatalog, name='certificates_catalog')
+        results = cat.searchResults(code=('CERT1','CERT1'))
+        results = [x for x in results] 
+        assert len(results) == 1
+        assert results[0] is self.certificate
+
+        # We can still find the course in the catalog
+        cat = queryUtility(ICatalog, name='courses_catalog')
+        results = cat.searchResults(code=('COURSE1','COURSE1'))
+        results = [x for x in results] #
+        assert len(results) == 1
+        assert results[0] is self.course
+
+        # We can still find the certificatecourse in the catalog
+        cat = queryUtility(ICatalog, name='certcourses_catalog')
+        results = cat.searchResults(course_code=('COURSE1','COURSE1'))
+        results = [x for x in results] 
+        assert len(results) == 1
+        assert results[0] is self.app['faculties']['fac2'][
+            'newdep'].certificates['CERT1']['COURSE1_100']
+
+        # We can find the student studying in the new department
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(depcode=('newdep','newdep'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+        # Messages are found in two log files
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('INFO - zope.mgr - K1000000 - Department moved'
+            in logcontent)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('INFO - zope.mgr - Department dep1 moved to fac2/newdep'
+            in logcontent)
+
+
+    def test_move_certificate(self):
+        self.app['faculties']['fac2'] = Faculty(code=u'fac2')
+        self.app['faculties']['fac2']['dep2'] = Department(code=u'dep2')
+        self.assertEqual(
+            self.app['faculties']['fac1']['dep1'].certificates['CERT1'],
+            self.certificate)
+
+        #self.certificate.moveCertificate('fac2', 'dep2', 'NEWCODE')
+
+        # We move the depart using the UtilityView
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/faculties/fac1/dep1/certificates/CERT1/move_certificate?fac=fac2&dep=dep2&cert=NEWCODE')
+
+        self.assertEqual(
+            self.app['faculties']['fac2']['dep2'].certificates['NEWCODE'],
+            self.certificate)
+        self.assertEqual(
+            [key for key in self.app['faculties']['fac2'][
+                'dep2'].certificates['NEWCODE'].keys()], [u'COURSE1_100'])
+        self.assertEqual(
+            self.student['studycourse'].certificate,
+            self.certificate)
+
+        # We can find the moved certificate in the catalog
+        cat = queryUtility(ICatalog, name='certificates_catalog')
+        results = cat.searchResults(code=('NEWCODE','NEWCODE'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.certificate
+
+        # Also certcourses_catalog has been updated
+        cat = queryUtility(ICatalog, name='certcourses_catalog')
+        results = cat.searchResults(course_code=('COURSE1','COURSE1'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['faculties']['fac2'][
+                'dep2'].certificates['NEWCODE']['COURSE1_100']
+
+        # We can find the student studying in the new department
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(depcode=('dep2','dep2'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+
+        # We can find the student studying the new course ...
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('NEWCODE','NEWCODE'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students'][self.student_id]
+        # ... but not the old course
+        results = cat.searchResults(certcode=('CERT1','CERT1'))
+        assert len(results) == 0
+
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('INFO - zope.mgr - K1000000 - Certificate moved'
+            in logcontent)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('INFO - zope.mgr - Certificate CERT1 moved to fac2/dep2/NEWCODE'
+            in logcontent)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_university.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_university.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/tests/test_university.py	(revision 16045)
@@ -0,0 +1,40 @@
+## $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 public API part of the university subpackage
+import unittest
+from waeup.kofa.university import *
+from waeup.kofa.testing import get_doctest_suite
+
+class UniversitySubpackageTests(unittest.TestCase):
+
+    def test_public_api(self):
+        names = globals().keys()
+        self.assertTrue('Course' in names)
+        self.assertTrue('CoursesContainer' in names)
+        self.assertTrue('Faculty' in names)
+        self.assertTrue('FacultiesContainer' in names)
+
+def test_suite():
+    # collect doctests for university subpackage
+    suite = get_doctest_suite(['doctests/certcourse.txt',])
+    # add local unittests (actually only one)
+    for testcase in [UniversitySubpackageTests,]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
+                testcase))
+    return suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/vocabularies.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/vocabularies.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/university/vocabularies.py	(revision 16045)
@@ -0,0 +1,127 @@
+## $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
+##
+"""Vocabularies and sources for the academic section.
+"""
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility, queryUtility
+from waeup.kofa.interfaces import (
+    SimpleKofaVocabulary, IKofaUtils, ContextualDictSourceFactoryBase)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.sourcefactory import SmartBasicContextualSourceFactory
+from waeup.kofa.utils.utils import KofaUtils
+
+course_levels = SimpleKofaVocabulary(
+    (_('None'),0),
+    (_('Pre-Studies'),10),
+    (_('100 (Year 1)'),100),
+    (_('200 (Year 2)'),200),
+    (_('300 (Year 3)'),300),
+    (_('400 (Year 4)'),400),
+    (_('500 (Year 5)'),500),
+    (_('600 (Year 6)'),600),
+    (_('700 (Year 7)'),700),
+    (_('800 (Year 8)'),800),
+    (_('900 (Year 9)'),900),
+    (_('Postgraduate Level'),999),
+    )
+
+class SemesterSource(ContextualDictSourceFactoryBase):
+    """An institution type source delivers semester and/or term descriptors.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'SEMESTER_DICT'
+
+class InstTypeSource(ContextualDictSourceFactoryBase):
+    """An institution type source delivers types of institutions
+    in the portal.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'INST_TYPES_DICT'
+
+class AppCatSource(ContextualDictSourceFactoryBase):
+    """A application category source delivers all application categories
+    provided in the portal.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'APP_CATS_DICT'
+
+class StudyModeSource(ContextualDictSourceFactoryBase):
+    """A study modes source delivers all study modes provided
+    in the portal.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'STUDY_MODES_DICT'
+
+class DegreeSource(ContextualDictSourceFactoryBase):
+    """A source for certificate degrees
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'DEGREES_DICT'
+
+    def getTitle(self, context, value):
+        utils = getUtility(IKofaUtils)
+        return "%s (%s)" % (getattr(utils, self.DICT_NAME)[value], value)
+
+class CourseSource(BasicSourceFactory):
+    """A course source delivers all courses inside the portal by looking
+       up a catalog.
+    """
+    def getValues(self):
+        catalog = getUtility(ICatalog, name='courses_catalog')
+        return sorted(list(
+                catalog.searchResults(
+                    code=(None, None))),key=lambda value: value.code)
+
+    def getToken(self, value):
+        return value.code
+
+    def getTitle(self, value):
+        return "%s - %s" % (value.code, value.title[:64])
+
+class CourseCategorySource(ContextualDictSourceFactoryBase):
+    """A course category source provides additional information on
+    the course.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'COURSE_CATEGORY_DICT'
+
+
+class CertificateSource(SmartBasicContextualSourceFactory):
+    """A certificate source delivers all certificates provided
+    in the portal.
+    """
+    def getValues(self, context):
+        catalog = getUtility(ICatalog, name='certificates_catalog')
+        return sorted(list(
+            catalog.searchResults(
+                code=(None, None))),
+                      key=lambda value: value.code)
+
+    def getToken(self, context, value):
+        return value.code
+
+    def getTitle(self, context, value):
+        return "%s - %s" % (value.code, value.title)
+
+class SpecialApplicationSource(ContextualDictSourceFactoryBase):
+    """A special application source delivers all types of
+    applications which are not linked with certificates.
+    """
+    #: name of dict to deliver from kofa utils.
+    DICT_NAME = 'SPECIAL_APP_DICT'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/userscontainer.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/userscontainer.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/userscontainer.py	(revision 16045)
@@ -0,0 +1,114 @@
+## $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
+##
+"""Users container for the Kofa portal.
+"""
+import grok
+from zope.event import notify
+from waeup.kofa.authentication import Account
+from waeup.kofa.interfaces import IUsersContainer
+from waeup.kofa.utils.logger import Logger
+from waeup.kofa.utils.batching import ExporterBase
+from waeup.kofa.interfaces import ICSVExporter
+from waeup.kofa.interfaces import MessageFactory as _
+
+
+class UsersContainer(grok.Container, Logger):
+    """A container for officers.
+
+    See interfaces.py and users.txt for extensive description.
+    """
+    grok.implements(IUsersContainer)
+    grok.require('waeup.manageUsers')
+
+    def addUser(self, name, password, title=None,
+                description=None, email=None, phone=None,
+                public_name= None, roles=[]):
+        """Add a new Account instance, created from parameters.
+        """
+        if title is None:
+            title = name
+        self[name] = Account(name=name, password=password, title=title,
+                             description=description, public_name=public_name,
+                             email=email, phone=phone, roles=roles)
+
+    def addAccount(self, account):
+        """Add the account passed.
+        """
+        self[account.name] = account
+
+    def delUser(self, name):
+        """Delete user, if an account with the given name exists.
+
+        Do not complain, if the name does not exist.
+        """
+        if name in self.keys():
+            del self[name]
+
+
+class UserExporter(grok.GlobalUtility, ExporterBase):
+    """The User Exporter exports all user accounts. It iterates over all
+    objects of the ``users`` container.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('users')
+
+    fields = ('name', 'title', 'public_name', 'description',
+              'email', 'phone', 'roles', 'local_roles', 'password')
+    title = _(u'Users')
+
+    def mangle_value(self, value, name, context=None):
+        """The mangler determines the local roles each user has and computes
+        a Python expression like:
+
+        ``{u'waeup.local.ClearanceOfficer': [u'faculties/ABC/', u'faculties/DEF/']}``
+        """
+        if name == 'local_roles' and context is not None:
+            local_roles = context.getLocalRoles()
+            value = {}
+            for role in local_roles.keys():
+                objects = local_roles[role]
+                object_list = []
+                for object in objects:
+                    obj= object
+                    path = ''
+                    while obj.__class__.__name__ != 'University':
+                        path = '%s/' % obj.__name__ + path
+                        obj = obj.__parent__
+                    object_list.append(path)
+                value[role] = object_list
+        return super(
+            UserExporter, self).mangle_value(
+            value, name, context=context)
+
+    def export(self, users, filepath=None):
+        """Export `users`, an iterable, as CSV file.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        for user in users:
+            self.write_item(user, writer)
+        return self.close_outfile(filepath, outfile)
+
+    def export_all(self, site, filepath=None):
+        """Export users into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        users = site.get('users', {})
+        return self.export(users.values(), filepath)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/batching.py	(revision 16045)
@@ -0,0 +1,938 @@
+## $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
+##
+"""Kofa components for batch processing.
+
+Batch processors eat CSV files to add, update or remove large numbers
+of certain kinds of objects at once.
+"""
+import grok
+import datetime
+import os
+import shutil
+import tempfile
+import time
+import unicodecsv
+import zc.async.interfaces
+from cStringIO import StringIO
+from persistent.list import PersistentList
+from zope.component import createObject, getUtility
+from zope.component.hooks import setSite
+from zope.interface import Interface, implementer
+from zope.schema import getFields
+from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
+from zope.event import notify
+from waeup.kofa.async import AsyncJob
+from waeup.kofa.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IJobManager,
+    ICSVExporter, IGNORE_MARKER, DuplicationError, JOB_STATUS_MAP,
+    IExportJobContainer, IExportJob, IExportContainerFinder)
+
+class BatchProcessor(grok.GlobalUtility):
+    """A processor to add, update, or remove data.
+
+    This is a non-active baseclass.
+    """
+    grok.implements(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    # Name used in pages and forms...
+    name = u'Non-registered base processor'
+
+    # Internal name...
+    util_name = ''
+
+    # Items for this processor need an interface with zope.schema fields.
+    iface = Interface
+
+    # The name must be the same as the util_name attribute in order to
+    # register this utility correctly.
+    grok.name(util_name)
+
+    # Headers needed to locate items...
+    location_fields = []
+
+    # A factory with this name must be registered...
+    factory_name = ''
+
+    @property
+    def required_fields(self):
+        """Required fields that have no default.
+
+        A list of names of field, whose value cannot be set if not
+        given during creation. Therefore these fields must exist in
+        input.
+
+        Fields with a default != missing_value do not belong to this
+        category.
+        """
+        result = []
+        for key, field in getFields(self.iface).items():
+            if key in self.location_fields:
+                continue
+            if field.default is not field.missing_value:
+                continue
+            if field.required:
+                result.append(key)
+        return result
+
+    @property
+    def req(self):
+        result = dict(
+            create = self.location_fields + self.required_fields,
+            update = self.location_fields,
+            remove = self.location_fields,
+        )
+        return result
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+                    self.location_fields + getFields(self.iface).keys())))
+
+    def getHeaders(self, mode='create'):
+        return self.available_fields
+
+    def checkHeaders(self, headerfields, mode='create'):
+        req = self.req[mode]
+        # Check for required fields...
+        for field in req:
+            if not field in headerfields:
+                raise FatalCSVError(
+                    "Need at least columns %s for import!" %
+                    ', '.join(["'%s'" % x for x in req]))
+        # Check for double fields. Cannot happen because this error is
+        # already catched in views
+        not_ignored_fields = [x for x in headerfields
+                              if not x.startswith('--')]
+        if len(set(not_ignored_fields)) < len(not_ignored_fields):
+            raise FatalCSVError(
+                "Double headers: each column name may only appear once.")
+        return True
+
+    def applyMapping(self, row, mapping):
+        """Apply mapping to a row of CSV data.
+        """
+        result = dict()
+        for key, replacement in mapping.items():
+            if replacement == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[replacement] = row[key]
+        return result
+
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+
+        """
+        result = dict()
+        reader = unicodecsv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in self.location_fields and mode == 'remove':
+                # Skip non-location fields when removing.
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
+    def stringFromErrs(self, errors, inv_errors):
+        result = []
+        for err in errors:
+            fieldname, message = err
+            result.append("%s: %s" % (fieldname, message))
+        for err in inv_errors:
+            result.append("invariant: %s" % err)
+        return '; '.join(result)
+
+    def callFactory(self, *args, **kw):
+        return createObject(self.factory_name)
+
+    def parentsExist(self, row, site):
+        """Tell whether the parent object for data in ``row`` exists.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def entryExists(self, row, site):
+        """Tell whether there already exists an entry for ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def getParent(self, row, site):
+        """Get the parent object for the entry in ``row``.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def getEntry(self, row, site):
+        """Get the object for the entry in ``row``.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def addEntry(self, obj, row, site):
+        """Add the entry given given by ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def delEntry(self, row, site):
+        """Delete entry given by ``row`` data.
+        """
+        raise NotImplementedError('method not implemented')
+
+    def checkCreateRequirements(self, parent, row, site):
+        """Checks requirements the parent object must fulfill when
+        a new subobject is being created.
+
+        This method is not used in case of updating or removing objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        This method is not used in case of deleting or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
+
+    def checkRemoveRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being removed.
+
+        This method is not used in case of updating or adding objects.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        return None
+
+    def updateEntry(self, obj, row, site, filename):
+        """Update obj to the values given in row.
+
+        Returns a string describing the fields changed.
+        """
+        changed = []
+        for key, value in row.items():
+            # Skip fields to be ignored.
+            if value == IGNORE_MARKER:
+                continue
+            # Skip fields not declared in interface and which are
+            # not yet attributes of existing objects. We can thus not
+            # add non-existing attributes here.
+            if not hasattr(obj, key):
+                continue
+            # DefaultObjectConverter.fromStringDict fails for
+            # list-of-choices fields because we are using a different
+            # widget for this combination. Thus the ListFieldConverter
+            # returns a useless dictionary which causes getWidgetsData to
+            # skip the field. The value in row remains unchanged. 
+            # We have to evaluate the string and replace the value here.
+            try:
+                evalvalue = eval(value)
+                if isinstance(evalvalue, list):
+                    value = evalvalue
+            except:
+                pass
+            try:
+                setattr(obj, key, value)
+            except AttributeError:
+                # Computed attributes can't be set.
+                continue
+            log_value = getattr(value, 'code', value)
+            changed.append('%s=%s' % (key, log_value))
+
+        # If any catalog is involved it must be updated.
+        #
+        # XXX: The event is also triggered when creating objects as
+        # updateEntry is called also when creating entries resulting
+        # in objectAdded and additional objectModified events.
+        if len(changed):
+            notify(grok.ObjectModifiedEvent(obj))
+
+        return ', '.join(changed)
+
+    def createLogfile(self, path, fail_path, num, warnings, mode, user,
+                      timedelta, logger=None):
+        """Write to log file.
+        """
+        if logger is None:
+            return
+        logger.info(
+            "processed: %s, %s mode, %s lines (%s successful/ %s failed), "
+            "%0.3f s (%0.4f s/item)" % (
+            path, mode, num, num - warnings, warnings,
+            timedelta, timedelta/(num or 1)))
+        return
+
+    def writeFailedRow(self, writer, row, warnings):
+        """Write a row with error messages to error CSV.
+
+        If warnings is a list of strings, they will be concatenated.
+        """
+        error_col = warnings
+        if isinstance(warnings, list):
+            error_col = ' / '.join(warnings)
+        row['--ERRORS--'] = error_col
+        writer.writerow(row)
+        return
+
+    def checkConversion(self, row, mode='ignore', ignore_empty=True):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name, mode=mode)
+        return errs, inv_errs, conv_dict
+
+
+    def emptyRow(self, row):
+        """Detect empty rows.
+        """
+        for value in row.values():
+            if not value in (None, IGNORE_MARKER) and value.strip():
+                return False
+        return True
+
+    def doImport(self, path, headerfields, mode='create', user='Unknown',
+                 logger=None, ignore_empty=True):
+        """In contrast to most other methods, `doImport` is not supposed to
+        be customized, neither in custom packages nor in derived batch
+        processor classes. Therefore, this is the only place where we
+        do import data.
+
+        Before this method starts creating or updating persistent data, it
+        prepares two more files in a temporary folder of the filesystem: (1)
+        a file for pending data with file extension ``.pending`` and (2)
+        a file for successfully processed data with file extension
+        ``.finished``. Then the method starts iterating over all rows of
+        the CSV file. Each row is treated as follows:
+
+        1. An empty row is skipped.
+
+        2. Empty strings or lists (``[]``) in the row are replaced by
+           ignore markers.
+
+        3. The `BatchProcessor.checkConversion` method validates and converts 
+           all values in the row. Conversion means the transformation of strings
+           into Python objects. For instance, number expressions have to be
+           transformed into integers, dates into datetime objects, phone number
+           expressions into phone number objects, etc. The converter returns a
+           dictionary with converted values or, if the validation of one of the
+           elements fails, an appropriate warning message. If the conversion
+           fails a pending record is created and stored in the pending data file
+           together with a warning message the converter has raised.
+
+        4. In **create mode** only:
+
+           The parent object must be found and a child
+           object with same object id must not exist. Otherwise the row
+           is skipped, a corresponding warning message is raised and a
+           record is stored in the pending data file.
+
+           The `BatchProcessor.checkCreateRequirements` method checks additional
+           requirements the parent object must fulfill before a new sububject
+           is being added. These requirements are not imposed by the data
+           type but the context of the object. For example, the course results
+           of graduated students must not changed by import, neither by 
+           creating nor updating or removing course tickets.
+
+           Now `doImport` tries to add the new object with the data
+           from the conversion dictionary. In some cases this
+           may fail and a `DuplicationError` is raised. For example, a new
+           payment ticket is created but the same payment for same session
+           has already been made. In this case the object id is unique, no
+           other object with same id exists, but making the 'same' payment
+           twice does not make sense. The import is skipped and a
+           record is stored in the pending data file.
+
+        5. In **update mode** only:
+
+           If the object can't be found, the row is skipped,
+           a ``no such entry`` warning message is raised and a record is
+           stored in the pending data file.
+
+           The `BatchProcessor.checkUpdateRequirements` method checks additional
+           requirements the object must fulfill before being updated. These
+           requirements are not imposed by the data type but the context
+           of the object. For example, post-graduate students have a different
+           registration workflow. With this method we do forbid certain workflow
+           transitions or states. 
+
+           Finally, `doImport` updates the existing object with the data
+           from the conversion dictionary.
+
+        6. In **remove mode** only:
+
+           If the object can't be found, the row is skipped,
+           a ``no such entry`` warning message is raised and a record is
+           stored in the pending data file.
+
+           The `BatchProcessor.checkRemoveRequirements` method checks additional
+           requirements the object must fulfill before being removed.
+           These requirements are not imposed by the data type but the context
+           of the object. For example, the course results of graduated students
+           must not changed by import, neither by creating nor updating or
+           removing course tickets.
+
+           Finally, `doImport` removes the existing object.
+
+        """
+        time_start = time.time()
+        self.checkHeaders(headerfields, mode)
+        mapping = self.getMapping(path, headerfields, mode)
+        reader = unicodecsv.DictReader(open(path, 'rb'))
+
+        temp_dir = tempfile.mkdtemp()
+
+        base = os.path.basename(path)
+        (base, ext) = os.path.splitext(base)
+        failed_path = os.path.join(temp_dir, "%s.pending%s" % (base, ext))
+        failed_headers = mapping.values()
+        failed_headers.append('--ERRORS--')
+        failed_writer = unicodecsv.DictWriter(open(failed_path, 'wb'),
+                                              failed_headers)
+        os.chmod(failed_path, 0664)
+        failed_writer.writerow(dict([(x,x) for x in failed_headers]))
+
+        finished_path = os.path.join(temp_dir, "%s.finished%s" % (base, ext))
+        finished_headers = mapping.values()
+        finished_writer = unicodecsv.DictWriter(open(finished_path, 'wb'),
+                                                finished_headers)
+        os.chmod(finished_path, 0664)
+        finished_writer.writerow(dict([(x,x) for x in finished_headers]))
+
+        num =0
+        num_warns = 0
+        site = grok.getSite()
+
+        for raw_row in reader:
+            num += 1
+            # Skip row if empty
+            if self.emptyRow(raw_row):
+                continue
+            string_row = self.applyMapping(raw_row, mapping)
+            if ignore_empty:
+                # Replace empty strings and empty lists with ignore-markers
+                for key, val in string_row.items():
+                    if val == '' or val == '[]':
+                        string_row[key] = IGNORE_MARKER
+            row = dict(string_row.items()) # create deep copy
+            errs, inv_errs, conv_dict = self.checkConversion(string_row, mode)
+            if errs or inv_errs:
+                num_warns += 1
+                conv_warnings = self.stringFromErrs(errs, inv_errs)
+                self.writeFailedRow(
+                    failed_writer, string_row, conv_warnings)
+                continue
+            row.update(conv_dict)
+
+            if mode == 'create':
+                if not self.parentsExist(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Not all parents do exist yet.")
+                    continue
+                if self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "This object already exists.")
+                    continue
+                parent = self.getParent(row, site)
+                create_errors = self.checkCreateRequirements(parent, row, site)
+                if create_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, create_errors)
+                    continue
+                obj = self.callFactory()
+                # Override all values in row, also
+                # student_ids and applicant_ids which have been
+                # generated in the respective __init__ methods before.
+                self.updateEntry(obj, row, site, base)
+                try:
+                    self.addEntry(obj, row, site)
+                except KeyError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, error.message)
+                    continue
+                except DuplicationError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, error.msg)
+                    continue
+                except FatalCSVError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, error.message)
+                    continue
+            elif mode == 'remove':
+                if not self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Cannot remove: no such entry")
+                    continue
+                obj = self.getEntry(row, site)
+                remove_errors = self.checkRemoveRequirements(obj, row, site)
+                if remove_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, remove_errors)
+                    continue
+                self.delEntry(row, site)
+            elif mode == 'update':
+                obj = self.getEntry(row, site)
+                if obj is None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Cannot update: no such entry")
+                    continue
+                update_errors = self.checkUpdateRequirements(obj, row, site)
+                if update_errors is not None:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row, update_errors)
+                    continue
+                try:
+                    self.updateEntry(obj, row, site, base)
+                except ConstraintNotSatisfied, err:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "ConstraintNotSatisfied: %s" % err)
+                    continue
+                except RequiredMissing, err:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "RequiredMissing: %s" % err)
+                    continue
+            finished_writer.writerow(string_row)
+
+        time_end = time.time()
+        timedelta = time_end - time_start
+
+        self.createLogfile(path, failed_path, num, num_warns, mode, user,
+                           timedelta, logger=logger)
+        failed_path = os.path.abspath(failed_path)
+        if num_warns == 0:
+            del failed_writer
+            os.unlink(failed_path)
+            failed_path = None
+        return (num, num_warns,
+                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 = unicodecsv.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.
+    """
+    grok.implements(ICSVExporter)
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'title', 'title_prefix')
+
+    #: The title under which this exporter will be displayed
+    #: (if registered as a utility)
+    title = 'Override this title'
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes.
+        """
+        if isinstance(value, bool):
+            value = value and '1' or '0'
+        elif isinstance(value, unicode):
+            # CSV writers like byte streams better than unicode
+            value = value.encode('utf-8')
+        elif isinstance(value, datetime.datetime):
+            #value = str(value)
+            value = str('%s#' % value) # changed 2014-07-06, see ticket #941
+        elif isinstance(value, datetime.date):
+            # Order is important here: check for date after datetime as
+            # datetimes are also dates.
+            #
+            # Append hash '#' to dates to circumvent unwanted excel automatic
+            value = str('%s#' % value)
+        elif value is None:
+            # None is not really representable in CSV files
+            value = ''
+        return value
+
+    def get_csv_writer(self, filepath=None):
+        """Get a CSV dict writer instance open for writing.
+
+        Returns a tuple (<writer>, <outfile>) where ``<writer>`` is a
+        :class:`csv.DictWriter` instance and outfile is the real file
+        which is written to. The latter is important when writing to
+        StringIO and can normally be ignored otherwise.
+
+        The returned file will already be filled with the header row.
+
+        Please note that if you give a filepath, the returned outfile
+        is open for writing only and you might have to close it before
+        reopening it for reading.
+        """
+        if filepath is None:
+            outfile = StringIO()
+        else:
+            outfile = open(filepath, 'wb')
+        writer = unicodecsv.DictWriter(outfile, self.fields)
+        writer.writerow(dict(zip(self.fields, self.fields))) # header
+        return writer, outfile
+
+    def write_item(self, obj, writer):
+        """Write a row extracted from `obj` into CSV file using `writer`.
+        """
+        row = {}
+        for name in self.fields:
+            value = getattr(obj, name, None)
+            value = self.mangle_value(value, name, obj)
+            row[name] = value
+        writer.writerow(row)
+        return
+
+    def close_outfile(self, filepath, outfile):
+        """Close outfile.
+        If filepath is None, the contents of outfile is returned.
+        """
+        outfile.seek(0)
+        if filepath is None:
+            return outfile.read()
+        outfile.close()
+        return
+
+    def get_filtered(self, site, **kw):
+        """Get datasets to export filtered by keyword arguments.
+        Returns an iterable.
+        """
+        raise NotImplementedError
+
+    def get_selected(self, site, selected):
+        """Get datasets to export for selected items
+        specified by a list of identifiers.
+        Returns an iterable.
+        """
+        raise NotImplementedError
+
+    def export(self, iterable, filepath=None):
+        """Export `iterable` as CSV file.
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        raise NotImplementedError
+
+    def export_all(self, site, filepath=None):
+        """Export all appropriate objects in `site` into `filepath` as
+        CSV data.
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        raise NotImplementedError
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export items denoted by `kw`.
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        data = self.get_filtered(site, **kw)
+        return self.export(data, filepath=filepath)
+
+    def export_selected(self, site, filepath=None, **kw):
+        """Export those items specified by a list of identifiers
+        called `selected`.
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        selected = kw.get('selected', [])
+        data = self.get_selected(site, selected)
+        return self.export(data, filepath=filepath)
+
+def export_job(site, exporter_name, **kw):
+    """Export all entries delivered by exporter and store it in a temp file.
+
+    `site` gives the site to search. It will be passed to the exporter
+    and also be set as 'current site' as the function is used in
+    asynchronous jobs which run in their own threads and have no site
+    set initially. Therefore `site` must also be a valid value for use
+    with `zope.component.hooks.setSite()`.
+
+    `exporter_name` is the utility name under which the desired
+    exporter was registered with the ZCA.
+
+    The resulting CSV file will be stored in a new temporary directory
+    (using :func:`tempfile.mkdtemp`). It will be named after the
+    exporter used with `.csv` filename extension.
+
+    Returns the path to the created CSV file.
+
+    .. note:: It is the callers responsibility to clean up the used
+              file and its parent directory.
+    """
+    setSite(site)
+    exporter = getUtility(ICSVExporter, name=exporter_name)
+    output_dir = tempfile.mkdtemp()
+    filename = '%s.csv' % exporter_name
+    output_path = os.path.join(output_dir, filename)
+    if kw == {}:
+        exporter.export_all(site, filepath=output_path)
+    elif kw.has_key('selected'):
+        exporter.export_selected(site, filepath=output_path, **kw)
+    else:
+        exporter.export_filtered(site, filepath=output_path, **kw)
+    return output_path
+
+class AsyncExportJob(AsyncJob):
+    """An IJob that exports data to CSV files.
+
+    `AsyncExportJob` instances are regular `AsyncJob` instances with a
+    different constructor API. Instead of a callable to execute, you
+    must pass a `site` and some `exporter_name` to trigger an export.
+
+    The real work is done when an instance of this class is put into a
+    queue. See :mod:`waeup.kofa.async` to learn more about
+    asynchronous jobs.
+
+    The `exporter_name` must be the name under which an ICSVExporter
+    utility was registered with the ZCA.
+
+    The `site` must be a valid site  or ``None``.
+
+    The result of an `AsyncExportJob` is the path to generated CSV
+    file. The file will reside in a temporary directory that should be
+    removed after being used.
+    """
+    grok.implements(IExportJob)
+
+    def __init__(self, site, exporter_name, *args, **kwargs):
+        super(AsyncExportJob, self).__init__(
+            export_job, site, exporter_name, *args, **kwargs)
+
+    @property
+    def finished(self):
+        """A job is marked `finished` if it is completed.
+
+        Please note: a finished report job does not neccessarily
+        provide an IReport result. See meth:`failed`.
+        """
+        return self.status == zc.async.interfaces.COMPLETED
+
+    @property
+    def failed(self):
+        """A report job is marked failed iff it is finished and the
+        result is None.
+
+        While a job is unfinished, the `failed` status is ``None``.
+
+        Failed jobs normally provide a `traceback` to examine reasons.
+        """
+        if not self.finished:
+            return None
+        if getattr(self, 'result', None) is None:
+            return True
+        return False
+
+class ExportJobContainer(object):
+    """A mix-in that provides functionality for asynchronous export jobs.
+    """
+    grok.implements(IExportJobContainer)
+    running_exports = PersistentList()
+
+    def start_export_job(self, exporter_name, user_id, *args, **kwargs):
+        """Start asynchronous export job.
+
+        `exporter_name` is the name of an exporter utility to be used.
+
+        `user_id` is the ID of the user that triggers the export.
+
+        The job_id is stored along with exporter name and user id in a
+        persistent list.
+
+        The method supports additional positional and keyword
+        arguments, which are passed as-is to the respective
+        :class:`AsyncExportJob`.
+
+        Returns the job ID of the job started.
+        """
+        site = grok.getSite()
+        manager = getUtility(IJobManager)
+        job = AsyncExportJob(site, exporter_name, *args, **kwargs)
+        job_id = manager.put(job)
+        # Make sure that the persisted list is stored in ZODB
+        self.running_exports = PersistentList(self.running_exports)
+        self.running_exports.append((job_id, exporter_name, user_id))
+        return job_id
+
+    def get_running_export_jobs(self, user_id=None):
+        """Get export jobs for user with `user_id` as list of tuples.
+
+        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
+        that order. The ``<exporter_name>`` is the utility name of the
+        used exporter.
+
+        If `user_id` is ``None``, all running jobs are returned.
+        """
+        entries = []
+        to_delete = []
+        manager = getUtility(IJobManager)
+        for entry in self.running_exports:
+            if user_id is not None and entry[2] != user_id:
+                continue
+            if manager.get(entry[0]) is None:
+                to_delete.append(entry)
+                continue
+            entries.append(entry)
+        if to_delete:
+            self.running_exports = PersistentList(
+                [x for x in self.running_exports if x not in to_delete])
+        return entries
+
+    def get_export_jobs_status(self, user_id=None):
+        """Get running/completed export jobs for `user_id` as list of tuples.
+
+        Each tuple holds ``<raw status>, <status translated>,
+        <exporter title>`` in that order, where ``<status
+        translated>`` and ``<exporter title>`` are translated strings
+        representing the status of the job and the human readable
+        title of the exporter used.
+        """
+        entries = self.get_running_export_jobs(user_id)
+        result = []
+        manager = getUtility(IJobManager)
+        for entry in entries:
+            job = manager.get(entry[0])
+            if job is None:
+                continue
+            status, status_translated = JOB_STATUS_MAP[job.status]
+            exporter_name = getUtility(ICSVExporter, name=entry[1]).title
+            result.append((status, status_translated, exporter_name))
+        return result
+
+    def delete_export_entry(self, entry):
+        """Delete the export denoted by `entry`.
+
+        Removes given entry from the local `running_exports` list and also
+        removes the regarding job via the local job manager.
+
+        `entry` must be a tuple ``(<job id>, <exporter name>, <user
+        id>)`` as created by :meth:`start_export_job` or returned by
+        :meth:`get_running_export_jobs`.
+        """
+        manager = getUtility(IJobManager)
+        job = manager.get(entry[0])
+        if job is not None:
+            # remove created export file
+            if isinstance(job.result, basestring):
+                if os.path.exists(os.path.dirname(job.result)):
+                    shutil.rmtree(os.path.dirname(job.result))
+        manager.remove(entry[0], self)
+        new_entries = [x for x in self.running_exports
+                       if x != entry]
+        self.running_exports = PersistentList(new_entries)
+        return
+
+    def entry_from_job_id(self, job_id):
+        """Get entry tuple for `job_id`.
+
+        Returns ``None`` if no such entry can be found.
+        """
+        for entry in self.running_exports:
+            if entry[0] == job_id:
+                return entry
+        return None
+
+class VirtualExportJobContainer(ExportJobContainer):
+    """A virtual export job container.
+
+    Virtual ExportJobContainers can be used as a mixin just like real
+    ExportJobContainer.
+
+    They retrieve and store data in the site-wide ExportJobContainer.
+
+    Functionality is currently entirely as for regular
+    ExportJobContainers, except that data is stored elsewhere.
+
+    VirtualExportJobContainers need a registered
+    IExportContainerFinder utility to find a suitable container for
+    storing data.
+    """
+    grok.implements(IExportJobContainer)
+
+    @property
+    def _site_container(self):
+        return getUtility(IExportContainerFinder)()
+
+    # The following is a simple trick. While ExportJobContainers store
+    # only one attribute in ZODB, it is sufficient to replace this
+    # attribute `running_exports` with a suitable manager to make the
+    # whole virtual container work like the original but with the data
+    # stored in the site-wide exports container. This way, virtual
+    # export containers provide the whole functionality of a regular
+    # exports container but store no data at all with themselves.
+    @property
+    def running_exports(self):
+        """Exports stored in the site-wide exports container.
+        """
+        return self._site_container.running_exports
+
+    @running_exports.setter
+    def running_exports(self, value):
+        self._site_container.running_exports = value
+
+    @running_exports.deleter
+    def running_exports(self):
+        del self._site_container.running_exports
+
+    @property
+    def logger(self):
+        return self._site_container.logger
+
+@implementer(IExportContainerFinder)
+class ExportContainerFinder(grok.GlobalUtility):
+    """Finder for local (site-wide) export container.
+    """
+
+    def __call__(self):
+        """Get the local export container-
+
+        If no site can be determined or the site provides no export
+        container, None is returned.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        return site.get('datacenter', None)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/browser.py	(revision 16045)
@@ -0,0 +1,258 @@
+## $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
+from waeup.kofa.university.interfaces import IDepartment, ICertificate
+from waeup.kofa.utils.helpers import reindex_cat
+
+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)
+        # Use Uli's updateIndexes method.
+        # cat.updateIndexes()
+        reindex_cat(cat)
+        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
+
+
+class MoveDepartment(UtilityView, grok.View):
+    """ View to move a department.
+
+    """
+    grok.context(IDepartment)
+    grok.name('move_department')
+    grok.require('waeup.managePortal')
+
+    def update(self,fac=None, dep=None):
+        if None in (fac, dep):
+            self.flash('Syntax: /move_department?fac=[new faculty code]&dep=[new department code]')
+            return
+        oldcode = self.context.code
+        try:
+            self.context.moveDepartment(fac, dep)
+        except:
+            self.flash('Error')
+            return
+        grok.getSite().logger.info(
+            "Department %s moved to %s/%s" % (oldcode, fac, dep))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class MoveCertificate(UtilityView, grok.View):
+    """ View to move a certificate.
+
+    """
+    grok.context(ICertificate)
+    grok.name('move_certificate')
+    grok.require('waeup.managePortal')
+
+    def update(self,fac=None, dep=None, cert=None):
+        if None in (fac, dep, cert):
+            self.flash('Syntax: /move_certificate?fac=[new faculty code]&dep=[new department code]&cert=[new certificate code]')
+            return
+        oldcode = self.context.code
+        try:
+            self.context.moveCertificate(fac, dep, cert)
+        except:
+            self.flash('Error')
+            return
+        grok.getSite().logger.info(
+            "Certificate %s moved to %s/%s/%s" % (oldcode, fac, dep, cert))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/converters.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/converters.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/converters.py	(revision 16045)
@@ -0,0 +1,323 @@
+## $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
+##
+"""Converters for zope.schema-based datatypes.
+"""
+import grok
+from zope.component import createObject
+from zope.formlib import form
+from zope.formlib.boolwidgets import CheckBoxWidget
+from zope.formlib.form import (
+    _widgetKey, WidgetInputError, ValidationError, InputErrors, expandPrefix)
+from zope.formlib.interfaces import IInputWidget, ConversionError
+from zope.interface import Interface
+from zope.publisher.browser import TestRequest
+from zope.schema.interfaces import IList
+from waeup.kofa.interfaces import (
+    IObjectConverter, IResultEntryField, IFieldConverter, SubjectSource,
+    GradeSource, DELETION_MARKER, IGNORE_MARKER)
+from waeup.kofa.schema.interfaces import IPhoneNumber
+from waeup.kofa.schoolgrades import ResultEntry
+
+class ExtendedCheckBoxWidget(CheckBoxWidget):
+    """A checkbox widget that supports more input values as True/False
+    markers.
+
+    The default bool widget expects the string 'on' as only valid
+    ``True`` value in HTML forms for bool fields.
+
+    This widget also accepts '1', 'true' and 'yes' for that. Also all
+    uppercase/lowecase combinations of these strings are accepted.
+
+    The widget still renders ``True`` to ``'on'`` when a form is
+    generated.
+    """
+    true_markers = ['1', 'true', 'on', 'yes']
+
+    def _toFieldValue(self, input):
+        """Convert from HTML presentation to Python bool."""
+        if not isinstance(input, basestring):
+            return False
+        return input.lower() in self.true_markers
+
+    def _getFormInput(self):
+        """Returns the form input used by `_toFieldValue`.
+
+        Return values:
+
+          ``'on'``  checkbox is checked
+          ``''``    checkbox is not checked
+          ``None``  form input was not provided
+
+        """
+        value = self.request.get(self.name)
+        if isinstance(value, basestring):
+            value = value.lower()
+        if value in self.true_markers:
+            return 'on'
+        elif self.name + '.used' in self.request:
+            return ''
+        else:
+            return None
+
+def getWidgetsData(widgets, form_prefix, data):
+    """Get data and validation errors from `widgets` for `data`.
+
+    Updates the dict in `data` with values from the widgets in
+    `widgets`.
+
+    Returns a list of tuples ``(<WIDGET_NAME>, <ERROR>)`` where
+    ``<WIDGET_NAME>`` is a widget name (normally the same as the
+    associated field name) and ``<ERROR>`` is the exception that
+    happened for that widget/field.
+
+    This is merely a copy from the same-named function in
+    :mod:`zope.formlib.form`. The only difference is that we also
+    store the fieldname for which a validation error happened in the
+    returned error list (what the original does not do).
+    """
+    errors = []
+    form_prefix = expandPrefix(form_prefix)
+
+    for input, widget in widgets.__iter_input_and_widget__():
+        if input and IInputWidget.providedBy(widget):
+            name = _widgetKey(widget, form_prefix)
+
+            if not widget.hasInput():
+                continue
+            try:
+                data[name] = widget.getInputValue()
+            except ValidationError, error:
+                error = ConversionError(u'Validation failed')
+                errors.append((name, error))
+            except WidgetInputError, error:
+                error = ConversionError(u'Invalid input')
+                errors.append((name, error))
+            except ConversionError, error:
+                errors.append((name, error))
+    return errors
+
+class DefaultFieldConverter(grok.Adapter):
+    grok.context(Interface)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        if prefix == 'form.sex' and isinstance(value, basestring):
+            value = value.lower()
+        return {prefix: value}
+
+class ListFieldConverter(grok.Adapter):
+    grok.context(IList)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        value_type = schema_field.value_type
+        try:
+            items = eval(value)
+        except:
+            return {prefix: value}
+        result = {'%s.count' % prefix: len(items)}
+        for num, item in enumerate(items):
+            sub_converter = IFieldConverter(value_type)
+            result.update(sub_converter.request_data(
+                unicode(num), unicode(item),
+                value_type, "%s.%s." % (prefix, num)))
+        return result
+
+class PhoneNumberFieldConverter(grok.Adapter):
+    """Convert strings into dict as expected from forms feeding PhoneWidget.
+
+    If you want strings without extra-checks imported, you can use
+    schema.TextLine in your interface instead of PhoneNumber.
+    """
+    grok.context(IPhoneNumber)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        parts = value.split('-', 2)
+        country = ''
+        area = ''
+        ext = ''
+        if len(parts) == 3:
+            country = parts[0]
+            area = parts[1]
+            ext = parts[2]
+        elif len(parts) == 2:
+            country = parts[0]
+            ext = parts[1]
+        else:
+            ext = value
+        result = {
+            u'%s.country' % prefix: country,
+            u'%s.area' % prefix: area,
+            u'%s.ext' % prefix: ext}
+        return result
+
+class ResultEntryConverter(grok.Adapter):
+    grok.context(IResultEntryField)
+    grok.implements(IFieldConverter)
+
+    def request_data(self, name, value, schema_field, prefix='',
+                     mode='create'):
+        """Turn CSV values into ResultEntry-compatible form data.
+
+        Expects as `value` a _string_ like ``(u'mysubject',
+        u'mygrade')`` and turns it into some dict like::
+
+          {
+            'form.grade.subject': u'9234896395...',
+            'form.grade.grade': u'7e67e9e777..'
+            }
+
+        where the values are tokens from appropriate sources.
+
+        Such dicts can be transformed into real ResultEntry objects by
+        input widgets used in converters.
+        """
+        try:
+            entry = ResultEntry.from_string(value)
+            subj, grade = entry.subject, entry.grade
+        except:
+            return {prefix: value}
+        # web forms send tokens instead of real values
+        s_token = SubjectSource().factory.getToken(subj)
+        g_token = GradeSource().factory.getToken(grade)
+        result = {
+            "%ssubject" % (prefix): s_token,
+            "%sgrade" % (prefix): g_token,
+            }
+        return result
+
+class DefaultObjectConverter(grok.Adapter):
+    """Turn string values into real values.
+
+    A converter can convert string values for objects that implement a
+    certain interface into real values based on the given interface.
+    """
+
+    grok.context(Interface)
+    grok.implements(IObjectConverter)
+
+    def __init__(self, iface):
+        self.iface = iface
+        # Omit known dictionaries since there is no widget available
+        # for dictionary schema fields
+        self.default_form_fields = form.Fields(iface).omit('description_dict')
+        return
+
+    def fromStringDict(self, data_dict, context, form_fields=None,
+                       mode='create'):
+        """Convert values in `data_dict`.
+
+        Converts data in `data_dict` into real values based on
+        `context` and `form_fields`.
+
+        `data_dict` is a mapping (dict) from field names to values
+        represented as strings.
+
+        The fields (keys) to convert can be given in optional
+        `form_fields`. If given, form_fields should be an instance of
+        :class:`zope.formlib.form.Fields`. Suitable instances are for
+        example created by :class:`grok.AutoFields`.
+
+        If no `form_fields` are given, a default is computed from the
+        associated interface.
+
+        The `context` can be an existing object (implementing the
+        associated interface) or a factory name. If it is a string, we
+        try to create an object using
+        :func:`zope.component.createObject`.
+
+        Returns a tuple ``(<FIELD_ERRORS>, <INVARIANT_ERRORS>,
+        <DATA_DICT>)`` where
+
+        ``<FIELD_ERRORS>``
+           is a list of tuples ``(<FIELD_NAME>, <ERROR>)`` for each
+           error that happened when validating the input data in
+           `data_dict`
+
+        ``<INVARIANT_ERRORS>``
+           is a list of invariant errors concerning several fields
+
+        ``<DATA_DICT>``
+           is a dict with the values from input dict converted.
+
+        If mode is ``'create'`` or ``'update'`` then some additional
+        filtering applies:
+
+        - values set to DELETION_MARKER are set to missing_value (or
+          default value if field is required) and
+
+        - values set to IGNORE_MARKER are ignored and thus not part of
+          the returned ``<DATA_DICT>``.
+
+        If errors happen, i.e. the error lists are not empty, always
+        an empty ``<DATA_DICT>`` is returned.
+
+        If ``<DATA_DICT>`` is non-empty, there were no errors.
+        """
+        if form_fields is None:
+            form_fields = self.default_form_fields
+
+        request = TestRequest(form={})
+        new_data = dict()
+        for key, val in data_dict.items():
+            field = form_fields.get(key, None)
+            if field is not None:
+                # let adapters to the respective schema fields do the
+                # further fake-request processing
+                schema_field = field.interface[field.__name__]
+                field_converter = IFieldConverter(schema_field)
+                if mode in ('update', 'create'):
+                    if val == IGNORE_MARKER:
+                        continue
+                    elif val == DELETION_MARKER:
+                        val = schema_field.missing_value
+                        if schema_field.required:
+                            val = schema_field.default
+                        new_data[key] = val
+                        continue
+                request.form.update(
+                    field_converter.request_data(
+                        key, val, schema_field, 'form.%s' % key)
+                    )
+            else:
+                request.form['form.%s' % key] = val
+
+        obj = context
+        if isinstance(context, basestring):
+            # If we log initialization transitions in the __init__
+            # method of objects, a second (misleading) log entry
+            # will be created here.
+            obj = createObject(context)
+
+        widgets = form.setUpInputWidgets(
+            form_fields, 'form', obj, request)
+
+        errors = getWidgetsData(widgets, 'form', new_data)
+
+        invariant_errors = form.checkInvariants(form_fields, new_data)
+
+        if errors or invariant_errors:
+            err_messages = [(key, err.args[0]) for key, err in errors]
+            invariant_errors = [err.message for err in invariant_errors]
+            return err_messages, invariant_errors, {}
+
+        return errors, invariant_errors, new_data
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.py	(revision 16045)
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+#
+# This file was automatically generated. Please do not edit
+#
+from waeup.kofa.interfaces import MessageFactory as _
+
+COUNTRIES = (
+    (_(u"Afghanistan"), u"AF"),
+    (_(u"Åland Islands"), u"AX"),
+    (_(u"Albania"), u"AL"),
+    (_(u"Algeria"), u"DZ"),
+    (_(u"American Samoa"), u"AS"),
+    (_(u"Andorra"), u"AD"),
+    (_(u"Angola"), u"AO"),
+    (_(u"Anguilla"), u"AI"),
+    (_(u"Antarctica"), u"AQ"),
+    (_(u"Antigua and Barbuda"), u"AG"),
+    (_(u"Argentina"), u"AR"),
+    (_(u"Armenia"), u"AM"),
+    (_(u"Aruba"), u"AW"),
+    (_(u"Australia"), u"AU"),
+    (_(u"Austria"), u"AT"),
+    (_(u"Azerbaijan"), u"AZ"),
+    (_(u"Bahamas"), u"BS"),
+    (_(u"Bahrain"), u"BH"),
+    (_(u"Bangladesh"), u"BD"),
+    (_(u"Barbados"), u"BB"),
+    (_(u"Belarus"), u"BY"),
+    (_(u"Belgium"), u"BE"),
+    (_(u"Belize"), u"BZ"),
+    (_(u"Benin"), u"BJ"),
+    (_(u"Bermuda"), u"BM"),
+    (_(u"Bhutan"), u"BT"),
+    (_(u"Bolivia, Plurinational State of"), u"BO"),
+    (_(u"Bonaire, Sint Eustatius and Saba"), u"BQ"),
+    (_(u"Bosnia and Herzegovina"), u"BA"),
+    (_(u"Botswana"), u"BW"),
+    (_(u"Bouvet Island"), u"BV"),
+    (_(u"Brazil"), u"BR"),
+    (_(u"British Indian Ocean Territory"), u"IO"),
+    (_(u"Brunei Darussalam"), u"BN"),
+    (_(u"Bulgaria"), u"BG"),
+    (_(u"Burkina Faso"), u"BF"),
+    (_(u"Burundi"), u"BI"),
+    (_(u"Cambodia"), u"KH"),
+    (_(u"Cameroon"), u"CM"),
+    (_(u"Canada"), u"CA"),
+    (_(u"Cape Verde"), u"CV"),
+    (_(u"Cayman Islands"), u"KY"),
+    (_(u"Central African Republic"), u"CF"),
+    (_(u"Chad"), u"TD"),
+    (_(u"Chile"), u"CL"),
+    (_(u"China"), u"CN"),
+    (_(u"Christmas Island"), u"CX"),
+    (_(u"Cocos (Keeling) Islands"), u"CC"),
+    (_(u"Colombia"), u"CO"),
+    (_(u"Comoros"), u"KM"),
+    (_(u"Congo"), u"CG"),
+    (_(u"Congo, the Democratic Republic of the"), u"CD"),
+    (_(u"Cook Islands"), u"CK"),
+    (_(u"Costa Rica"), u"CR"),
+    (_(u"Côte d'Ivoire"), u"CI"),
+    (_(u"Croatia"), u"HR"),
+    (_(u"Cuba"), u"CU"),
+    (_(u"Curaçao"), u"CW"),
+    (_(u"Cyprus"), u"CY"),
+    (_(u"Czech Republic"), u"CZ"),
+    (_(u"Denmark"), u"DK"),
+    (_(u"Djibouti"), u"DJ"),
+    (_(u"Dominica"), u"DM"),
+    (_(u"Dominican Republic"), u"DO"),
+    (_(u"Ecuador"), u"EC"),
+    (_(u"Egypt"), u"EG"),
+    (_(u"El Salvador"), u"SV"),
+    (_(u"Equatorial Guinea"), u"GQ"),
+    (_(u"Eritrea"), u"ER"),
+    (_(u"Estonia"), u"EE"),
+    (_(u"Ethiopia"), u"ET"),
+    (_(u"Falkland Islands (Malvinas)"), u"FK"),
+    (_(u"Faroe Islands"), u"FO"),
+    (_(u"Fiji"), u"FJ"),
+    (_(u"Finland"), u"FI"),
+    (_(u"France"), u"FR"),
+    (_(u"French Guiana"), u"GF"),
+    (_(u"French Polynesia"), u"PF"),
+    (_(u"French Southern Territories"), u"TF"),
+    (_(u"Gabon"), u"GA"),
+    (_(u"Gambia"), u"GM"),
+    (_(u"Georgia"), u"GE"),
+    (_(u"Germany"), u"DE"),
+    (_(u"Ghana"), u"GH"),
+    (_(u"Gibraltar"), u"GI"),
+    (_(u"Greece"), u"GR"),
+    (_(u"Greenland"), u"GL"),
+    (_(u"Grenada"), u"GD"),
+    (_(u"Guadeloupe"), u"GP"),
+    (_(u"Guam"), u"GU"),
+    (_(u"Guatemala"), u"GT"),
+    (_(u"Guernsey"), u"GG"),
+    (_(u"Guinea"), u"GN"),
+    (_(u"Guinea-bissau"), u"GW"),
+    (_(u"Guyana"), u"GY"),
+    (_(u"Haiti"), u"HT"),
+    (_(u"Heard Island and Mcdonald Islands"), u"HM"),
+    (_(u"Holy See (Vatican City State)"), u"VA"),
+    (_(u"Honduras"), u"HN"),
+    (_(u"Hong Kong"), u"HK"),
+    (_(u"Hungary"), u"HU"),
+    (_(u"Iceland"), u"IS"),
+    (_(u"India"), u"IN"),
+    (_(u"Indonesia"), u"ID"),
+    (_(u"Iran, Islamic Republic of"), u"IR"),
+    (_(u"Iraq"), u"IQ"),
+    (_(u"Ireland"), u"IE"),
+    (_(u"Isle of Man"), u"IM"),
+    (_(u"Israel"), u"IL"),
+    (_(u"Italy"), u"IT"),
+    (_(u"Jamaica"), u"JM"),
+    (_(u"Japan"), u"JP"),
+    (_(u"Jersey"), u"JE"),
+    (_(u"Jordan"), u"JO"),
+    (_(u"Kazakhstan"), u"KZ"),
+    (_(u"Kenya"), u"KE"),
+    (_(u"Kiribati"), u"KI"),
+    (_(u"Korea, Democratic People's Republic of"), u"KP"),
+    (_(u"Korea, Republic of"), u"KR"),
+    (_(u"Kuwait"), u"KW"),
+    (_(u"Kyrgyzstan"), u"KG"),
+    (_(u"Lao People's Democratic Republic"), u"LA"),
+    (_(u"Latvia"), u"LV"),
+    (_(u"Lebanon"), u"LB"),
+    (_(u"Lesotho"), u"LS"),
+    (_(u"Liberia"), u"LR"),
+    (_(u"Libya"), u"LY"),
+    (_(u"Liechtenstein"), u"LI"),
+    (_(u"Lithuania"), u"LT"),
+    (_(u"Luxembourg"), u"LU"),
+    (_(u"Macao"), u"MO"),
+    (_(u"Macedonia, the Former Yugoslav Republic of"), u"MK"),
+    (_(u"Madagascar"), u"MG"),
+    (_(u"Malawi"), u"MW"),
+    (_(u"Malaysia"), u"MY"),
+    (_(u"Maldives"), u"MV"),
+    (_(u"Mali"), u"ML"),
+    (_(u"Malta"), u"MT"),
+    (_(u"Marshall Islands"), u"MH"),
+    (_(u"Martinique"), u"MQ"),
+    (_(u"Mauritania"), u"MR"),
+    (_(u"Mauritius"), u"MU"),
+    (_(u"Mayotte"), u"YT"),
+    (_(u"Mexico"), u"MX"),
+    (_(u"Micronesia, Federated States of"), u"FM"),
+    (_(u"Moldova, Republic of"), u"MD"),
+    (_(u"Monaco"), u"MC"),
+    (_(u"Mongolia"), u"MN"),
+    (_(u"Montenegro"), u"ME"),
+    (_(u"Montserrat"), u"MS"),
+    (_(u"Morocco"), u"MA"),
+    (_(u"Mozambique"), u"MZ"),
+    (_(u"Myanmar"), u"MM"),
+    (_(u"Namibia"), u"NA"),
+    (_(u"Nauru"), u"NR"),
+    (_(u"Nepal"), u"NP"),
+    (_(u"Netherlands"), u"NL"),
+    (_(u"New Caledonia"), u"NC"),
+    (_(u"New Zealand"), u"NZ"),
+    (_(u"Nicaragua"), u"NI"),
+    (_(u"Niger"), u"NE"),
+    (_(u"Nigeria"), u"NG"),
+    (_(u"Niue"), u"NU"),
+    (_(u"Norfolk Island"), u"NF"),
+    (_(u"Northern Mariana Islands"), u"MP"),
+    (_(u"Norway"), u"NO"),
+    (_(u"Oman"), u"OM"),
+    (_(u"Pakistan"), u"PK"),
+    (_(u"Palau"), u"PW"),
+    (_(u"Palestinian Territory, Occupied"), u"PS"),
+    (_(u"Panama"), u"PA"),
+    (_(u"Papua New Guinea"), u"PG"),
+    (_(u"Paraguay"), u"PY"),
+    (_(u"Peru"), u"PE"),
+    (_(u"Philippines"), u"PH"),
+    (_(u"Pitcairn"), u"PN"),
+    (_(u"Poland"), u"PL"),
+    (_(u"Portugal"), u"PT"),
+    (_(u"Puerto Rico"), u"PR"),
+    (_(u"Qatar"), u"QA"),
+    (_(u"Réunion"), u"RE"),
+    (_(u"Romania"), u"RO"),
+    (_(u"Russian Federation"), u"RU"),
+    (_(u"Rwanda"), u"RW"),
+    (_(u"Saint Barthélemy"), u"BL"),
+    (_(u"Saint Helena, Ascension and Tristan Da Cunha"), u"SH"),
+    (_(u"Saint Kitts and Nevis"), u"KN"),
+    (_(u"Saint Lucia"), u"LC"),
+    (_(u"Saint Martin (French Part)"), u"MF"),
+    (_(u"Saint Pierre and Miquelon"), u"PM"),
+    (_(u"Saint Vincent and the Grenadines"), u"VC"),
+    (_(u"Samoa"), u"WS"),
+    (_(u"San Marino"), u"SM"),
+    (_(u"Sao Tome and Principe"), u"ST"),
+    (_(u"Saudi Arabia"), u"SA"),
+    (_(u"Senegal"), u"SN"),
+    (_(u"Serbia"), u"RS"),
+    (_(u"Seychelles"), u"SC"),
+    (_(u"Sierra Leone"), u"SL"),
+    (_(u"Singapore"), u"SG"),
+    (_(u"Sint Maarten (Dutch Part)"), u"SX"),
+    (_(u"Slovakia"), u"SK"),
+    (_(u"Slovenia"), u"SI"),
+    (_(u"Solomon Islands"), u"SB"),
+    (_(u"Somalia"), u"SO"),
+    (_(u"South Africa"), u"ZA"),
+    (_(u"South Georgia and the South Sandwich Islands"), u"GS"),
+    (_(u"South Sudan"), u"SS"),
+    (_(u"Spain"), u"ES"),
+    (_(u"Sri Lanka"), u"LK"),
+    (_(u"Sudan"), u"SD"),
+    (_(u"Suriname"), u"SR"),
+    (_(u"Svalbard and Jan Mayen"), u"SJ"),
+    (_(u"Swaziland"), u"SZ"),
+    (_(u"Sweden"), u"SE"),
+    (_(u"Switzerland"), u"CH"),
+    (_(u"Syrian Arab Republic"), u"SY"),
+    (_(u"Taiwan, Province of China"), u"TW"),
+    (_(u"Tajikistan"), u"TJ"),
+    (_(u"Tanzania, United Republic of"), u"TZ"),
+    (_(u"Thailand"), u"TH"),
+    (_(u"Timor-leste"), u"TL"),
+    (_(u"Togo"), u"TG"),
+    (_(u"Tokelau"), u"TK"),
+    (_(u"Tonga"), u"TO"),
+    (_(u"Trinidad and Tobago"), u"TT"),
+    (_(u"Tunisia"), u"TN"),
+    (_(u"Turkey"), u"TR"),
+    (_(u"Turkmenistan"), u"TM"),
+    (_(u"Turks and Caicos Islands"), u"TC"),
+    (_(u"Tuvalu"), u"TV"),
+    (_(u"Uganda"), u"UG"),
+    (_(u"Ukraine"), u"UA"),
+    (_(u"United Arab Emirates"), u"AE"),
+    (_(u"United Kingdom"), u"GB"),
+    (_(u"United States"), u"US"),
+    (_(u"United States Minor Outlying Islands"), u"UM"),
+    (_(u"Uruguay"), u"UY"),
+    (_(u"Uzbekistan"), u"UZ"),
+    (_(u"Vanuatu"), u"VU"),
+    (_(u"Venezuela, Bolivarian Republic of"), u"VE"),
+    (_(u"Viet Nam"), u"VN"),
+    (_(u"Virgin Islands, British"), u"VG"),
+    (_(u"Virgin Islands, U.S."), u"VI"),
+    (_(u"Wallis and Futuna"), u"WF"),
+    (_(u"Western Sahara"), u"EH"),
+    (_(u"Yemen"), u"YE"),
+    (_(u"Zambia"), u"ZM"),
+    (_(u"Zimbabwe"), u"ZW"),
+)
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.txt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.txt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/countries.txt	(revision 16045)
@@ -0,0 +1,255 @@
+Received on 2012-03-23 00:50:06.989914 
+from http://www.iso.org/iso/list-en1-semic-3.txt
+-----------------
+This list states the country names (official short names in English) in alphabetical order as given in ISO 3166-1 and the corresponding ISO 3166-1-alpha-2 code elements. The list is updated whenever a change to the official code list in ISO 3166-1 is effected by the ISO 3166/MA. It lists 249 official short names and code elements. One line of text contains one entry. A country name and its code element are separated by a semicolon (;).
+
+AFGHANISTAN;AF
+ÅLAND ISLANDS;AX
+ALBANIA;AL
+ALGERIA;DZ
+AMERICAN SAMOA;AS
+ANDORRA;AD
+ANGOLA;AO
+ANGUILLA;AI
+ANTARCTICA;AQ
+ANTIGUA AND BARBUDA;AG
+ARGENTINA;AR
+ARMENIA;AM
+ARUBA;AW
+AUSTRALIA;AU
+AUSTRIA;AT
+AZERBAIJAN;AZ
+BAHAMAS;BS
+BAHRAIN;BH
+BANGLADESH;BD
+BARBADOS;BB
+BELARUS;BY
+BELGIUM;BE
+BELIZE;BZ
+BENIN;BJ
+BERMUDA;BM
+BHUTAN;BT
+BOLIVIA, PLURINATIONAL STATE OF;BO
+BONAIRE, SINT EUSTATIUS AND SABA;BQ
+BOSNIA AND HERZEGOVINA;BA
+BOTSWANA;BW
+BOUVET ISLAND;BV
+BRAZIL;BR
+BRITISH INDIAN OCEAN TERRITORY;IO
+BRUNEI DARUSSALAM;BN
+BULGARIA;BG
+BURKINA FASO;BF
+BURUNDI;BI
+CAMBODIA;KH
+CAMEROON;CM
+CANADA;CA
+CAPE VERDE;CV
+CAYMAN ISLANDS;KY
+CENTRAL AFRICAN REPUBLIC;CF
+CHAD;TD
+CHILE;CL
+CHINA;CN
+CHRISTMAS ISLAND;CX
+COCOS (KEELING) ISLANDS;CC
+COLOMBIA;CO
+COMOROS;KM
+CONGO;CG
+CONGO, THE DEMOCRATIC REPUBLIC OF THE;CD
+COOK ISLANDS;CK
+COSTA RICA;CR
+CÔTE D'IVOIRE;CI
+CROATIA;HR
+CUBA;CU
+CURAÇAO;CW
+CYPRUS;CY
+CZECH REPUBLIC;CZ
+DENMARK;DK
+DJIBOUTI;DJ
+DOMINICA;DM
+DOMINICAN REPUBLIC;DO
+ECUADOR;EC
+EGYPT;EG
+EL SALVADOR;SV
+EQUATORIAL GUINEA;GQ
+ERITREA;ER
+ESTONIA;EE
+ETHIOPIA;ET
+FALKLAND ISLANDS (MALVINAS);FK
+FAROE ISLANDS;FO
+FIJI;FJ
+FINLAND;FI
+FRANCE;FR
+FRENCH GUIANA;GF
+FRENCH POLYNESIA;PF
+FRENCH SOUTHERN TERRITORIES;TF
+GABON;GA
+GAMBIA;GM
+GEORGIA;GE
+GERMANY;DE
+GHANA;GH
+GIBRALTAR;GI
+GREECE;GR
+GREENLAND;GL
+GRENADA;GD
+GUADELOUPE;GP
+GUAM;GU
+GUATEMALA;GT
+GUERNSEY;GG
+GUINEA;GN
+GUINEA-BISSAU;GW
+GUYANA;GY
+HAITI;HT
+HEARD ISLAND AND MCDONALD ISLANDS;HM
+HOLY SEE (VATICAN CITY STATE);VA
+HONDURAS;HN
+HONG KONG;HK
+HUNGARY;HU
+ICELAND;IS
+INDIA;IN
+INDONESIA;ID
+IRAN, ISLAMIC REPUBLIC OF;IR
+IRAQ;IQ
+IRELAND;IE
+ISLE OF MAN;IM
+ISRAEL;IL
+ITALY;IT
+JAMAICA;JM
+JAPAN;JP
+JERSEY;JE
+JORDAN;JO
+KAZAKHSTAN;KZ
+KENYA;KE
+KIRIBATI;KI
+KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF;KP
+KOREA, REPUBLIC OF;KR
+KUWAIT;KW
+KYRGYZSTAN;KG
+LAO PEOPLE'S DEMOCRATIC REPUBLIC;LA
+LATVIA;LV
+LEBANON;LB
+LESOTHO;LS
+LIBERIA;LR
+LIBYA;LY
+LIECHTENSTEIN;LI
+LITHUANIA;LT
+LUXEMBOURG;LU
+MACAO;MO
+MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF;MK
+MADAGASCAR;MG
+MALAWI;MW
+MALAYSIA;MY
+MALDIVES;MV
+MALI;ML
+MALTA;MT
+MARSHALL ISLANDS;MH
+MARTINIQUE;MQ
+MAURITANIA;MR
+MAURITIUS;MU
+MAYOTTE;YT
+MEXICO;MX
+MICRONESIA, FEDERATED STATES OF;FM
+MOLDOVA, REPUBLIC OF;MD
+MONACO;MC
+MONGOLIA;MN
+MONTENEGRO;ME
+MONTSERRAT;MS
+MOROCCO;MA
+MOZAMBIQUE;MZ
+MYANMAR;MM
+NAMIBIA;NA
+NAURU;NR
+NEPAL;NP
+NETHERLANDS;NL
+NEW CALEDONIA;NC
+NEW ZEALAND;NZ
+NICARAGUA;NI
+NIGER;NE
+NIGERIA;NG
+NIUE;NU
+NORFOLK ISLAND;NF
+NORTHERN MARIANA ISLANDS;MP
+NORWAY;NO
+OMAN;OM
+PAKISTAN;PK
+PALAU;PW
+PALESTINIAN TERRITORY, OCCUPIED;PS
+PANAMA;PA
+PAPUA NEW GUINEA;PG
+PARAGUAY;PY
+PERU;PE
+PHILIPPINES;PH
+PITCAIRN;PN
+POLAND;PL
+PORTUGAL;PT
+PUERTO RICO;PR
+QATAR;QA
+RÉUNION;RE
+ROMANIA;RO
+RUSSIAN FEDERATION;RU
+RWANDA;RW
+SAINT BARTHÉLEMY;BL
+SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA;SH
+SAINT KITTS AND NEVIS;KN
+SAINT LUCIA;LC
+SAINT MARTIN (FRENCH PART);MF
+SAINT PIERRE AND MIQUELON;PM
+SAINT VINCENT AND THE GRENADINES;VC
+SAMOA;WS
+SAN MARINO;SM
+SAO TOME AND PRINCIPE;ST
+SAUDI ARABIA;SA
+SENEGAL;SN
+SERBIA;RS
+SEYCHELLES;SC
+SIERRA LEONE;SL
+SINGAPORE;SG
+SINT MAARTEN (DUTCH PART);SX
+SLOVAKIA;SK
+SLOVENIA;SI
+SOLOMON ISLANDS;SB
+SOMALIA;SO
+SOUTH AFRICA;ZA
+SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS;GS
+SOUTH SUDAN;SS
+SPAIN;ES
+SRI LANKA;LK
+SUDAN;SD
+SURINAME;SR
+SVALBARD AND JAN MAYEN;SJ
+SWAZILAND;SZ
+SWEDEN;SE
+SWITZERLAND;CH
+SYRIAN ARAB REPUBLIC;SY
+TAIWAN, PROVINCE OF CHINA;TW
+TAJIKISTAN;TJ
+TANZANIA, UNITED REPUBLIC OF;TZ
+THAILAND;TH
+TIMOR-LESTE;TL
+TOGO;TG
+TOKELAU;TK
+TONGA;TO
+TRINIDAD AND TOBAGO;TT
+TUNISIA;TN
+TURKEY;TR
+TURKMENISTAN;TM
+TURKS AND CAICOS ISLANDS;TC
+TUVALU;TV
+UGANDA;UG
+UKRAINE;UA
+UNITED ARAB EMIRATES;AE
+UNITED KINGDOM;GB
+UNITED STATES;US
+UNITED STATES MINOR OUTLYING ISLANDS;UM
+URUGUAY;UY
+UZBEKISTAN;UZ
+VANUATU;VU
+VENEZUELA, BOLIVARIAN REPUBLIC OF;VE
+VIET NAM;VN
+VIRGIN ISLANDS, BRITISH;VG
+VIRGIN ISLANDS, U.S.;VI
+WALLIS AND FUTUNA;WF
+WESTERN SAHARA;EH
+YEMEN;YE
+ZAMBIA;ZM
+ZIMBABWE;ZW
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/country_helpers.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/country_helpers.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/country_helpers.py	(revision 16045)
@@ -0,0 +1,107 @@
+"""Tools to create country lists.
+
+Helpers that can retrieve files from ISO servers with current ISO
+codes and country names.
+
+Also helpers for turning this lists into a runnable Python module
+(countries.py) are provided.
+
+These helpers are meant to be used from the debug schell. You can use
+them like this::
+
+  $ ./bin/kofactl debug
+  >>> from waeup.kofa.utils.country_helpers import update_list
+  >>> from waeup.kofa.utils.country_helpers import update_module
+  >>> update_list()
+  >>> update_module()
+
+This will retrieve a current list of countries and their ISO codes
+from iso-committee servers, store the file locally (countries.txt) and
+afterwards compile a valid Python module (countries.py) out of the
+list.
+
+"""
+import datetime
+import os
+import urllib
+
+SRC_URL = 'http://www.iso.org/iso/list-en1-semic-3.txt'
+COUNTRY_LIST = os.path.join(os.path.dirname(__file__), 'countries.txt')
+COUNTRY_MODULE = os.path.join(os.path.dirname(__file__), 'countries.py')
+
+TEMPLATE = """# -*- coding: utf-8 -*-
+#
+# This file was automatically generated. Please do not edit
+#
+from waeup.kofa.interfaces import MessageFactory as _
+
+COUNTRIES = ${COUNTRY_LIST}
+"""
+
+def mangle_country_name(name):
+    """Turn uppercase country names into capitalized.
+
+    Some special cases are handled as well ('of', 'the', etc.)
+    """
+    name = name.replace('(', '( ') # handle letters following brackets
+    name = ' '.join([x.capitalize() for x in name.split(' ')])
+    name = name.replace('( ', '(')
+    for term in ['Of', ' And', 'The']:
+        name = name.replace(term, term.lower())
+    name = name.replace('U.s.', 'U.S.')
+    name = name.replace("D'i", "d'I") # Cote d'Ivoire
+    return name
+
+def update_list():
+    """Update the local country.txt list.
+
+    File is received from iso-servers.
+    """
+    print "Receiving list from %s" % SRC_URL
+    contents = urllib.urlopen(SRC_URL).read()
+    print "Storing list at %s" % COUNTRY_LIST
+    fp = open(COUNTRY_LIST, 'wb')
+    fp.write('Received on %s \r\nfrom %s\r\n-----------------\r\n' % (
+        datetime.datetime.now(), SRC_URL))
+    fp.write(contents)
+    fp.close()
+    print "Done."
+    return
+
+def mangle_list():
+    """Read country list and extract countries and their ISO-codes.
+
+    Returns an iterable of tuples (<COUNTRY>, <ISO-CODE>).
+    """
+    lines = open(COUNTRY_LIST, 'rb').readlines()
+    read_header = False
+    for num, line in enumerate(lines):
+        if read_header is False and line != '\r\n':
+            continue
+        if read_header is False:
+            read_header = True
+            continue
+        if line == '\r\n':
+            continue
+        line = line.decode('iso-8859-1').strip()
+        try:
+            country, code = line.split(';', 1)
+        except:
+            print "Could not process: line %s:", num, line
+            continue
+        yield mangle_country_name(country), code
+
+def update_module(template=TEMPLATE, module_path=COUNTRY_MODULE):
+    """Update local countries.py with data from countries.txt
+    """
+    print "Parsing countries and their ISO codes..."
+    c_list = "(\n"
+    for country, code in mangle_list():
+        c_list = c_list + u'    (_(u"%s"), u"%s"),\n' % (country, code)
+    c_list += ')\n'
+    contents = template.replace('${COUNTRY_LIST}', c_list)
+    contents = contents.encode('utf-8')
+    print "Write new module:", module_path
+    open(module_path, 'wb').write(contents)
+    print "Done."
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/degrees.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/degrees.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/degrees.py	(revision 16045)
@@ -0,0 +1,12 @@
+DEGREES_DICT = {
+    'BA': 'Bachelor of Arts',
+    'BAGR': 'Bachelor of Agriculture',
+    'LLB': 'Bachelor of Laws',
+    'MA': 'Master of Arts',
+    'MBA': 'Master of Business Administration',
+    'MUSM': 'Master of Music',
+    'LLM': 'Master of Laws',
+    'DBA': 'Doctor of Business Administration',
+    'PHD': 'Doctor of Philosophy',
+    'MD': 'Doctor of Medicine',
+    }
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/helpers.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/helpers.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/helpers.py	(revision 16045)
@@ -0,0 +1,967 @@
+## $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
+##
+"""General helper functions for Kofa.
+"""
+import unicodecsv as csv  # XXX: csv ops should move to dedicated module.
+import datetime
+import imghdr
+import logging
+import os
+import pytz
+import re
+import shutil
+import tempfile
+import transaction
+import grok
+from cStringIO import StringIO
+from docutils.core import publish_string
+from HTMLParser import HTMLParser
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.interface.interface import Method, Attribute
+from zope.intid.interfaces import IIntIds
+from zope.schema import getFieldNames
+from zope.schema.fieldproperty import FieldProperty
+from zope.security.interfaces import NoInteraction
+from zope.security.management import getInteraction
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.formlib.widget import renderElement
+
+
+BUFSIZE = 8 * 1024
+
+
+def remove_file_or_directory(filepath):
+    """Remove a file or directory.
+
+    Different to :func:`shutil.rmtree` we also accept not existing
+    paths (returning silently) and if a dir turns out to be a regular
+    file, we remove that.
+    """
+    filepath = os.path.abspath(filepath)
+    if not os.path.exists(filepath):
+        return
+    if os.path.isdir(filepath):
+        shutil.rmtree(filepath)
+    else:
+        os.unlink(filepath)
+    return
+
+
+def copy_filesystem_tree(src, dst, overwrite=False, del_old=False):
+    """Copy contents of directory src to directory dst.
+
+    Both directories must exists.
+
+    If `overwrite` is true, any same named objects will be
+    overwritten. Otherwise these files will not be touched.
+
+    If `del_old` is true, copied files and directories will be removed
+    from the src directory.
+
+    This functions returns a list of non-copied files.
+
+    Unix hidden files and directories (starting with '.') are not
+    processed by this function.
+    """
+    if not os.path.exists(src):
+        raise ValueError('source path does not exist: %s' % src)
+    if not os.path.exists(dst):
+        raise ValueError('destination path does not exist: %s' % dst)
+    if not os.path.isdir(src):
+        raise ValueError('source path is not a directory: %s' % src)
+    if not os.path.isdir(dst):
+        raise ValueError('destination path is not a directory: %s' % dst)
+    not_copied = []
+    for item in os.listdir(src):
+        if item.startswith('.'):
+            continue  # We do not copy hidden stuff...
+        itemsrc = os.path.join(src, item)
+        itemdst = os.path.join(dst, item)
+
+        if os.path.exists(itemdst):
+            if overwrite is True:
+                remove_file_or_directory(itemdst)
+            else:
+                not_copied.append(item)
+                continue
+
+        if os.path.isdir(itemsrc):
+            shutil.copytree(itemsrc, itemdst)
+        else:
+            shutil.copy2(itemsrc, itemdst)
+        if del_old:
+            remove_file_or_directory(itemsrc)
+    return not_copied
+
+
+def get_inner_HTML_part(html_code):
+    """Return the 'inner' part of a complete HTML snippet.
+
+    If there is a form part, get this.
+
+    If there is no form part, try to return the body part contents.
+
+    If there is no body, return as-is.
+
+    Let's see how that works. If we deliver some doc with form, we
+    will get that form only:
+
+       >>> doc = '<html><form>My Form</form>Outside the form</html>'
+       >>> get_inner_HTML_part(doc)
+       '<form>My Form</form>'
+
+    No form? Then seek for a body part and get the contents:
+
+       >>> doc = '<html><body>My Body</body>Trailing Trash</html>'
+       >>> get_inner_HTML_part(doc)
+       'My Body'
+
+    If none of these is included, return what we got:
+
+       >>> doc = '<html>without body nor form</html>'
+       >>> get_inner_HTML_part(doc)
+       '<html>without body nor form</html>'
+
+    """
+
+    try:
+        result = re.match('^.+(<form[^\>]*>.*</form>).+$', html_code,
+                          re.DOTALL).groups()[0]
+        return result
+    except AttributeError:
+        # No <form> part included
+        try:
+            result = re.match('^.+<body[^\>]*>(.*)</body>.*$', html_code,
+                              re.DOTALL).groups()[0]
+            return result
+        except AttributeError:
+            # No <form> and no <body> tag...
+            pass
+    return html_code
+
+
+class FactoryBase(grok.GlobalUtility):
+    """A factory for things.
+
+    This is a baseclass for easier creation of factories. Factories
+    are utilities that are registered under a certain name and return
+    instances of certain classes when called.
+
+    In :mod:`waeup.kofa` we use factories extensively for
+    batching. While processing a batch some processors looks up a
+    factory to create real-world instances that then get filled with
+    data from imported CSV files.
+
+    To get rid of reimplementing the same stuff over and over again,
+    most notably the methods defined here, we offer this base class
+    (which will *not* be registered as a factory itself).
+
+    Real factories can then be created like this:
+
+       >>> import grok
+       >>> from waeup.kofa.utils.helpers import FactoryBase
+       >>> class MyObject(object):
+       ...   # Some class we want to get instances of.
+       ...   pass
+       >>> class MyObjectFactory(FactoryBase):
+       ...   # This is the factory for MyObject instances
+       ...   grok.name(u'waeup.kofa.factory.MyObject')
+       ...   factory = MyObject
+
+    That's it. It is essential to set the ``factory`` attribute, which
+    will determine the class of which instances should be created when
+    called. The given name must even be unique amongst all utilities
+    registered during runtime. While you can pick any name you like
+    you might want to prepend ``waeup.kofa.factory.`` to the name
+    string to make sure it does not clash with names of other
+    utilities one day.
+
+    Before all this works we have to grok the baseclass once and our
+    freshly defined factory. This executes all the component
+    registration stuff we don't want to do ourselves. In daily use
+    this is done automatically on startup of a :mod:`waeup.kofa`
+    system.
+
+       >>> grok.testing.grok('waeup.kofa.utils.helpers')
+       >>> grok.testing.grok_component(
+       ...    'MyObjectFactory', MyObjectFactory
+       ...  )
+       True
+
+    After grokking we (and processors) can create objects without
+    knowing about the location of the real class definition, just by
+    the factory name:
+
+       >>> from zope.component import createObject
+       >>> obj = createObject('waeup.kofa.factory.MyObject')
+       >>> isinstance(obj, MyObject)
+       True
+
+    We can also use the regular utility lookups to find our new
+    factory:
+
+       >>> from zope.component import getUtility
+       >>> from zope.component.interfaces import IFactory
+       >>> factory = getUtility(
+       ...   IFactory, name='waeup.kofa.factory.MyObject'
+       ...   )
+       >>> isinstance(factory, MyObjectFactory)
+       True
+
+    And this factory generates `MyObject` instances:
+
+       >>> obj = factory()
+       >>> isinstance(obj, MyObject)
+       True
+
+    """
+    grok.baseclass()  # Do not grok this class, do not register us.
+    grok.implements(IFactory)
+    # You can override any of the following attributes in derived
+    # classes. The `grok.name` setting *must* even be set to some
+    # unique value.
+    grok.name(u'waeup.Factory')
+    title = u"Create instances of ``factory``.",
+    description = u"This factory instantiates new applicant instances."
+    factory = None
+
+    def __call__(self, *args, **kw):
+        """The main factory function.
+
+        Returns an instance of the requested object.
+        """
+        return self.factory()
+
+    def getInterfaces(self):
+        # Required by IFactory
+        return implementedBy(self.factory)
+
+
+def ReST2HTML_w_warnings(source_string):
+    """Convert a reStructuredText string to HTML preserving warnings.
+
+    Returns a tuple ``(<HTML_CODE>, <WARNINGS>)``, both being
+    strings. Where ``<HTML_CODE>`` is the HTML code generated from the
+    source string (in unicode), ``<WARNINGS>`` is a string containing
+    any warning messages or ``None``.
+
+    Regular multi-line ReStructuredText strings will be returned as
+    HTML code:
+
+        >>> from waeup.kofa.utils.helpers import ReST2HTML
+        >>> source = '''
+        ... Headline
+        ... ========
+        ...
+        ... - A list item
+        ... - Another item
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html, warnings = ReST2HTML_w_warnings(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <ul class="simple">
+        <li>A list item</li>
+        <li>Another item</li>
+        </ul>
+        <p>Thanks for watching!</p>
+        </div>
+
+    Here no warnings happened, so the `warnings` are ``None``:
+
+        >>> warnings is None
+        True
+
+    If warnings happen then they can be retrieved in the returned
+    ``warnings``. We try to render an erraneous document:
+
+        >>> source = '''
+        ... Headline
+        ... ======
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html, warnings = ReST2HTML_w_warnings(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+        >>> print warnings
+        <string>:3: (WARNING/2) Title underline too short.
+        <BLANKLINE>
+        Headline
+        ======
+        <BLANKLINE>
+
+    As you can see, the warnings are not displayed inline the document
+    but can be retrieved from the returned warnings, which is a string
+    or ``None``.
+    """
+    warnings = StringIO()
+    fulldoc = publish_string(
+        source_string, writer_name='html4css1',
+        settings_overrides={
+            'report_level': 0,
+            'warning_stream': warnings,
+            })
+    warnings.seek(0)
+    warning_msgs = warnings.read()
+    if warning_msgs:
+        # Render again, this time with no warnings inline...
+        fulldoc = publish_string(
+        source_string, writer_name='html4css1',
+        settings_overrides={
+            'report_level': 10000,
+            'halt_level': 10000,
+            'warning_stream': warnings,
+            })
+    if warning_msgs == '':
+        warning_msgs = None
+    result = get_inner_HTML_part(fulldoc).strip()
+    if not isinstance(result, unicode):
+        result = result.decode('utf-8')
+    return result, warning_msgs
+
+
+def ReST2HTML(source_string):
+    """Render a string containing ReStructuredText to HTML.
+
+    Any warnings about too short headings, etc. are silently
+    discarded. Use :func:`ReST2HTML_w_warnings` if you want to get any
+    warnings.
+
+    The returned string will be unicode.
+
+    A regular document will be rendered like this:
+
+        >>> source = '''
+        ... Headline
+        ... ========
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html = ReST2HTML(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+    A document with markup problems (here: the underline is too short)
+    will look similar:
+
+        >>> source = '''
+        ... Headline
+        ... ======
+        ...
+        ... Thanks for watching!
+        ... '''
+        >>> html = ReST2HTML(source)
+        >>> print html
+        <div class="document" id="headline">
+        <h1 class="title">Headline</h1>
+        <BLANKLINE>
+        <p>Thanks for watching!</p>
+        </div>
+
+    """
+    html, warnings = ReST2HTML_w_warnings(source_string)
+    return html
+
+
+def attrs_to_fields(cls, omit=[]):
+    """Set class attributes and bind them to the data definitions
+    specified in the interface by turning the attributes into FieldProperty
+    instances.
+
+    With Python >= 2.6 we can even use this function as a class decorator.
+
+    `omit` is a list of field names that should _not_ be turned into
+    field properties. This is useful for properties and the like.
+    """
+    iface = list(implementedBy(cls))[0]
+    for field_name in getFieldNames(iface):
+        if field_name in omit:
+            continue
+        field_property = FieldProperty(iface[field_name])
+        # Set proper docstring for the API docs.
+        field_property.__doc__ = iface[field_name].title + ' (computed attribute)'
+        setattr(cls, field_name, field_property)
+    return cls
+
+
+def get_current_principal():
+    """Get the 'current' principal.
+
+    This method works without a request. Examining a request is the
+    regular (and recommended) way to get a principal involved
+    'currently'.
+
+    Use this method only if you really have no access to the current
+    request.
+
+    Returns ``None`` when no principal is involved (for instance
+    during tests).
+    """
+    try:
+        principal = getInteraction().participations[0].principal
+    except NoInteraction:
+        return None
+    except IndexError:  # No participations present
+        return None
+    return principal
+
+
+def cmp_files(file_descr1, file_descr2):
+    """Compare two files by their file descriptors.
+
+    Returns ``True`` if both are equal, ``False`` otherwise.
+    """
+    file_descr1.seek(0)
+    file_descr2.seek(0)
+    while True:
+        b1 = file_descr1.read(BUFSIZE)
+        b2 = file_descr2.read(BUFSIZE)
+        if b1 != b2:
+            return False
+        if not b1:
+            return True
+
+
+def string_from_bytes(number):
+    """Turn a number into some textual representation.
+
+      Examples:
+
+        >>> string_from_bytes(1)
+        u'1 byte(s)'
+
+        >>> string_from_bytes(1025)
+        u'1 KB'
+
+        >>> string_from_bytes(1.5 * 1024*1024)
+        u'1.50 MB'
+
+        >>> string_from_bytes(673.286 * 1024**3)
+        u'673.29 GB'
+
+    """
+    if number < 1024:
+        return u'%s byte(s)' % (str(number),)
+    elif number < 1024 ** 2:
+        return u'%s KB' % (number / 1024,)
+    elif number < 1024 ** 3:
+        return u'%.2f MB' % (number / 1024 ** 2,)
+    return u'%.2f GB' % (number / 1024 ** 3,)
+
+
+def file_size(file_like_obj):
+    """Determine file size in most effective manner.
+
+    Returns the number of bytes in a file. This function works for
+    both, real files as well as file-like objects like cStringIO based
+    'files'.
+
+    Example:
+
+      >>> from cStringIO import StringIO
+      >>> file_size(StringIO('my file content'))
+      15
+
+    Please note that this function expects the file-like object passed
+    in to be at first reading position (it does no seek(0)) and that
+    when finished the file pointer might be at end of file.
+    """
+    if hasattr(file_like_obj, 'fileno'):
+        return os.fstat(file_like_obj.fileno())[6]
+    file_like_obj.seek(0, 2)  # seek to last position in file
+    return file_like_obj.tell()
+
+
+def get_user_account(request):
+    """Return local user account.
+    """
+    principal_id = request.principal.id
+    authenticator = getUtility(IAuthenticatorPlugin, name='users')
+    account = authenticator.getAccount(principal_id)
+    return account
+
+
+def iface_names(iface, omit=[], exclude_attribs=True, exclude_methods=True):
+    """Get all attribute names of an interface.
+
+    Searches also base interfaces.
+
+    Names of fields that are pure attributes
+    (i.e. zope.interface.Attribute) or methods are excluded by
+    default.
+
+    Names of typical fields derived from zope.schema are included.
+
+    The `omit` paramter can give a list of names to exclude.
+
+    Returns an unsorted list of strings.
+    """
+    ifaces = set((iface,))
+    # Collect all interfaces (also bases) recursively
+    while True:
+        ext_ifaces = set(ifaces)
+        for iface in ext_ifaces:
+            ext_ifaces = set.union(ext_ifaces, set(iface.getBases()))
+        if ext_ifaces == ifaces:
+            # No new interfaces found, list complete
+            break
+        ifaces = ext_ifaces
+    # Collect (filtered) names of collected interfaces
+    result = []
+    for iface in ifaces:
+        for name, descr in iface.namesAndDescriptions():
+            if name in omit:
+                continue
+            if exclude_attribs and descr.__class__ is Attribute:
+                continue
+            if exclude_methods and isinstance(descr, Method):
+                continue
+            if name in result:
+                continue
+            result.append(name)
+    return result
+
+
+def get_sorted_preferred(tuples_iterable, preferred_list):
+    """Get a list of tuples (<TITLE>,<TOKEN>) with values in
+    `preferred_list` put in front.
+
+    The rest of the tuples iterable is returned in orginal order. This
+    is useful for putting default entries on top of (already sorted)
+    lists of choice values, for instance when sorting countries and
+    their code.
+
+    Sample:
+
+    We have a list of tuples with uppercase 'titles' and lowercase
+    'tokens'. This list is already sorted but we want certain values
+    of this list to show up before other values. For instance we want
+    to see the 'C' entry to come first.
+
+      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
+      ...                       ['c'])
+      (('C', 'c'), ('A', 'a'), ('B', 'b'))
+
+    i.e. the entry with 'c' as second value moved to head of result.
+
+    We can also require multiple entries at head of list:
+
+      >>> get_sorted_preferred([('A','a'), ('B','b'), ('C','c')],
+      ...                       ['b', 'c'])
+      (('B', 'b'), ('C', 'c'), ('A', 'a'))
+
+    We required the 'b' entry to come before the 'c' entry and then
+    the rest of the input list. That's what we got.
+
+    The result is returned as a tuple of tuples to keep order of values.
+    """
+    result = [None for x in preferred_list]
+    for title, code in tuples_iterable:
+        if code in preferred_list:
+            index = preferred_list.index(code)
+            result[index] = (title, code)
+        else:
+            result.append((title, code))
+    return tuple(result)
+
+
+def now(tz=None):
+    """Get current datetime in timezone of `tz`.
+
+    If `tz`, a `tzinfo` instance, is None, UTC time is returned.
+
+    `tz` should be a timezone as defined in pytz.
+    """
+    return to_timezone(datetime.datetime.utcnow(), tz=tz)
+
+
+def to_timezone(dt, tz=None):
+    """Shift datetime into timezone `tz`.
+
+    If datetime `dt` contains no `tzinfo` (i.e. it is 'naive'), it is
+    assumed to be UTC.
+
+    If no `tz` is given, shift to UTC is performed.
+
+    If `dt` is not a datetime.datetime, the input value is returned
+    unchanged.
+    """
+    if not isinstance(dt, datetime.datetime):
+        return dt
+    if tz is None:
+        tz = pytz.utc
+    if dt.tzinfo is None:
+        dt = pytz.utc.localize(dt)
+    return tz.normalize(dt.tzinfo.normalize(dt).astimezone(tz))
+
+
+def imghdr_test_fpm(h, f):
+    """FPM fileformat test.
+
+    The `fpm` fileformat is the binary fingerprint data as created by
+    `libfprint`.
+    """
+    if len(h) >= 3 and h[:3] == 'FP1':
+        return 'fpm'
+
+
+#: Add test function in stdlib's imghdr tests.
+imghdr.tests.append(imghdr_test_fpm)
+
+
+def get_fileformat(path, bytestream=None):
+    """Try to determine the file format of a given media file.
+
+    Although checks done here are not done very thoroughly, they make
+    no assumptions about the filetype by looking at its filename
+    extension or similar. Instead they check header data to comply
+    with common known rules (Magic Words).
+
+    If bytestream is not `None` the `path` is ignored.
+
+    Returns filetype as string (something like ``'jpg'``) if
+    file-format can be recognized, ``None`` else.
+
+    Tested recognized filetypes currently are `jpg`, `png`, `fpm`, and
+    `pdf`.
+
+    More filetypes (though untested in waeup.kofa) are automatically
+    recognized because we deploy the stdlib `imghdr` library. See this
+    module's docs for a complete list of filetypes recognized.
+    """
+    if path is None and bytestream is None:
+        return None
+
+    img_type = None
+    if bytestream is not None:
+        img_type = imghdr.what(path, bytestream)
+    else:
+        img_type = imghdr.what(path)
+    for name, replacement in (('jpeg', 'jpg'), ('tiff', 'tif')):
+        if img_type == name:
+            img_type = replacement
+    return img_type
+
+
+def check_pdf(bytestream, file):
+    """Tell whether a file or bytestream is a PDF file.
+
+    Works as a test/plugin for the stdlib `imghdr` library.
+    """
+    if file is not None:
+        file.seek(0)
+        bytestream = file.read(4)
+        file.seek(0)
+
+    if bytestream.startswith('%PDF'):
+        return 'pdf'
+    return None
+
+# register check_pdf as header check function with `imghdr`
+if check_pdf not in imghdr.tests:
+    imghdr.tests.append(check_pdf)
+
+
+def merge_csv_files(path1, path2):
+    """Merge two CSV files into one (appending).
+
+    CSV data from `path2` will be merged into `path1` csv file. This
+    is a bit like 'appending' data from path2 to data from path1.
+
+    The path of the resulting temporary file will be returned.
+
+    In the result file data from `path2` will always come _after_ data
+    from `path1`.
+
+    **Caution**: It is the _callers_ responsibility to remove the
+    result file (which is created by tempfile.mkstemp) after usage.
+
+    This CSV file merging copes with different column orders in both
+    CSV files and even with different column sets in both files.
+
+    Also broken/empty CSV files can be handled.
+    """
+    # sniff the col names
+    try:
+        row10 = csv.DictReader(open(path1, 'rb')).next()
+    except StopIteration:
+        row10 = dict()
+    try:
+        row20 = csv.DictReader(open(path2, 'rb')).next()
+    except StopIteration:
+        row20 = dict()
+    fieldnames = sorted(list(set(row10.keys() + row20.keys())))
+    # now read/write the real data
+    reader1 = csv.DictReader(open(path1, 'rb'))
+    reader2 = csv.DictReader(open(path2, 'rb'))
+    wp, tmp_path = tempfile.mkstemp()
+    writer = csv.DictWriter(os.fdopen(wp, 'wb'), fieldnames)
+    writer.writerow(dict((x, x) for x in fieldnames))  # header
+    for row in reader1:
+        writer.writerow(row)
+    for row in reader2:
+        writer.writerow(row)
+    return tmp_path
+
+
+def product(sequence, start=1):
+    """Returns the product of a sequence of numbers (_not_ strings)
+    multiplied by the parameter `start` (defaults to 1). If the
+    sequence is empty, returns 0.
+    """
+    if not len(sequence):
+        return 0
+    result = start
+    for item in sequence:
+        result *= item
+    return result
+
+
+class NullHandler(logging.Handler):
+    """A logging NullHandler.
+
+    Does not log anything. Useful if you want to shut up a log.
+
+    Defined here for backwards compatibility with Python < 2.7.
+    """
+    def emit(self, record):
+        pass
+
+
+def check_csv_charset(iterable):
+    """Check contents of `iterable` regarding valid CSV encoding and
+    trailing whitespaces in data.
+
+    `iterable` is expected to be an iterable on _rows_ (not
+    chars). This is true for instance for
+    filehandlers. `zope.publisher.browser.FileUpload` instances are
+    _not_ iterable, unfortunately.
+
+    Returns line num of first illegal char or ``None``. Line nums
+    start counting with 1 (not zero). Returns -1 if data contain
+    trailing whitespaces.
+    """
+    linenum = 1
+    try:
+        reader = csv.DictReader(iterable)
+        for row in reader:
+            linenum += 1
+            for value in row.values():
+                if value.endswith(' '):
+                    return -1
+    except UnicodeDecodeError:
+        return linenum
+    except:
+        return linenum + 1
+    return None
+
+
+class MemInfo(dict):
+    """A dict with access to its items like if they are attributes.
+    """
+    __getattr__ = dict.__getitem__
+    __setattr__ = dict.__setitem__
+    __delattr__ = dict.__delitem__
+
+
+def get_meminfo(src="/proc/meminfo"):
+    """Get local memory info as provided in /proc/meminfo.
+
+    Entries in /proc/meminfo are available as MemInfo attributes.
+
+    By default we lookup a file /proc/meminfo. Another path can be
+    lines = open(src, 'r').read()passed in as `src` parameter. In this
+    case `src` must be a regular file and contain meminfo-style data.
+
+    If the given `src` (or `/proc/meminfo`) are not available, `None`
+    lines = open(src, 'r').read()is returned.
+    """
+    if not os.path.isfile(src):
+        return None
+    lines = open(src, 'r').read().splitlines()
+    result = MemInfo()
+    for line in lines:
+        key, value = line.split(':', 1)
+        value = int(value.split(' kB', 1)[0])
+        result[key] = value
+    return result
+
+def html2dict(value=None,portal_language='en'):
+    """Transforms a localized HTML text string into a dictionary.
+
+    Different languages must be separated by ``>>xy<<`` whereas
+    xy is the language code. Text parts without correct leading
+    language separator - usually the first part has no language
+    descriptor - are interpreted as texts in the portal's language.
+    """
+    try:
+        parts = value.split('>>')
+    except:
+        return {}
+    elements = {}
+    lang = portal_language
+    for part in parts:
+        if part[2:4] == u'<<':
+            lang = str(part[0:2].lower())
+            text = part[4:]
+            elements[lang] = renderElement(u'div id="html"',
+                contents=text)
+        else:
+            text = part
+            elements[lang] = renderElement(u'div id="html"',
+                contents=text)
+    return elements
+
+def rest2dict(value=None,portal_language='en'):
+    """Transforms a localized REST text string into a dictionary.
+
+    Different languages must be separated by ``>>xy<<``` whereas
+    xy is the language code. Text parts without correct leading
+    language separator - usually the first part has no language
+    descriptor - are interpreted as texts in the portal's language.
+    """
+    try:
+        parts = value.split('>>')
+    except:
+        return {}
+    elements = {}
+    lang = portal_language
+    for part in parts:
+        if part[2:4] == u'<<':
+            lang = str(part[0:2].lower())
+            text = part[4:]
+            elements[lang] = renderElement(u'div id="rest"',
+                contents=ReST2HTML(text))
+        else:
+            text = part
+            elements[lang] = renderElement(u'div id="rest"',
+                contents=ReST2HTML(text))
+    return elements
+
+
+
+class FormVarParser(HTMLParser):
+    """An HTML form parser that extracts keys and values.
+
+       Fed with an HTML document, we parse all starttags and check for each,
+       whether it provides a `name` and a `value` attribute. If so, the
+       values of the respective attributes are stored in instance var
+       `form_vars` as a dict entry.
+    """
+
+    def __init__(self):
+        HTMLParser.__init__(self)  # old-style class - no super()
+        self.form_vars = {}
+
+    def handle_starttag(self, tag, attrs):
+        tag_attrs = {}
+        for key, val in attrs:
+            tag_attrs[key] = val
+        if 'name' in tag_attrs and 'value' in tag_attrs:
+            self.form_vars[tag_attrs['name']] = unicode(tag_attrs['value'])
+
+
+def extract_formvars(html_code):
+    """Extract keys and values from an HTML form as dict.
+
+       No text, no values::
+
+         >>> extract_formvars("")
+         {}
+
+       Simple input tags normally provide name and value::
+
+         >>> extract_formvars("<input type='text' name='foo' value='bar'>")
+         {'foo': u'bar'}
+
+       The sample doc we stored in tests is a bit more difficult::
+
+         >>> html_path = os.path.join(os.path.dirname(__file__),
+         ...                          'tests', 'sample_response.html')
+         >>> html_code = open(html_path, 'r').read()
+         >>> import pprint
+         >>> pprint.pprint(extract_formvars(html_code))
+         {'AMOUNT': u'100',
+         ...
+          'TRANS_NUM': u'01ESA20190916134824YA3YJ8'}
+
+    """
+    result = {}
+    parser = FormVarParser()
+    parser.feed(html_code)
+    return parser.form_vars
+
+
+def get_catalog_docids(cat):
+    """Get all docids for a given catalog `cat`.
+
+    Catalogs store the ids of objects they index. Get all of these object ids.
+    This function works at least for catalogs that provide field- and text
+    indexes only.
+    """
+    result = []
+    for index in cat.values():
+        try:
+            # FieldIndexes
+            result.extend(list(index._rev_index.keys()))
+        except AttributeError:
+            # TextIndexes
+            result.extend(list(index.index._docwords.keys()))
+    return set(result)
+
+
+def reindex_cat(cat):
+    """Reindex all objects stored in a catalog `cat`.
+
+    Regular catalogs try to reindex all stored object ids of a ZODB when asked
+    to reindex all contents. That can be overkill. This function reindexes only
+    those objects, that were already stored in a catalog. It was tested for
+    catalogs with at least 650000 objects.
+
+    Please note, that reindexing catalgos, can take a considerable amount of
+    time. 100.000 objects took about 12 minutes to reindex on a 16 GB machine.
+    """
+    d1 = datetime.datetime.now()
+    print("Collecting doc ids...")
+    uidutil = getUtility(IIntIds, context=cat)
+    uids = get_catalog_docids(cat)
+    print("Found %s entries..." % len(uids))
+    for n, docid in enumerate(uids):
+        ob = uidutil.getObject(docid)
+        cat.index_doc(docid, ob)
+        # indexes can become huge. commit changes every 5000th round to
+        # keep the memory footprint of catalogs `updateIndex` manageable
+        if not n % 5000:
+            transaction.commit()
+    d2 = datetime.datetime.now()
+    print("Finished. %s" % (d2 - d1))
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/logger.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/logger.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/logger.py	(revision 16045)
@@ -0,0 +1,389 @@
+## $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
+##
+"""
+Convenience stuff for logging.
+
+Main component of :mod:`waeup.kofa.utils.logging` is a mix-in class
+:class:`waeup.kofa.utils.logging.Logger`. Classes derived (also) from
+that mix-in provide a `logger` attribute that returns a regular Python
+logger logging to a rotating log file stored in the datacenter storage
+path.
+
+Deriving components (classes) should set their own `logger_name` and
+`logger_filename` attribute.
+
+The `logger_name` tells under which name the logger should be
+registered Python-wise. This is usually a dotted name string like
+``waeup.kofa.${sitename}.mycomponent`` which should be unique. If you
+pick a name already used by another component, trouble is ahead. The
+``${sitename}`` chunk of the name can be set literally like this. The
+logger machinery will turn it into some real site name at time of
+logging.
+
+The `logger_filename` attribute tells how the logfile should be
+named. This should be some base filename like
+``mycomponent.log``. Please note, that some logfile names are already
+used: ``main.log``, ``applications.log``, and ``datacenter.log``.
+
+The `Logger` mix-in also cares for updating the logging handlers when
+a datacenter location moves. That means you do not have to write your
+own event handlers for the purpose. Just derive from `Logger`, set
+your `logger_name` and `logger_filename` attribute and off you go::
+
+  from waeup.kofa.utils.logger import Logger
+
+  class MyComponent(object, Logger):
+      # Yes that's a complete working class
+      logger_name = 'waeup.kofa.${sitename}.mycomponent
+      logger_filename = 'mycomponent.log'
+
+      def do_something(self):
+           # demomstrate how to use logging from methods
+           self.logger.info('About to do something')
+           try:
+               # Do something here
+           except IOError:
+               self.logger.warn('Something went wrong')
+               return
+           self.logger.info('I did it')
+
+As you can see from that example, methods of the class can log
+messages simply by calling `self.logger`.
+
+The datacenter and its storage are created automatically when you
+create a :class:`waeup.kofa.app.University`. This also means that
+logging with the `Logger` mix-in will work only inside so-called sites
+(`University` instances put into ZODB are such `sites`).
+
+Other components in this module help to make everything work.
+"""
+import os
+import grok
+import logging
+from logging.handlers import WatchedFileHandler
+from string import Template
+from zope import schema
+from zope.component import queryUtility
+from zope.interface import Interface, Attribute, implements
+from waeup.kofa.interfaces import (
+    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
+from waeup.kofa.utils.helpers import get_current_principal
+
+#: Default logging level (`logging.INFO`)
+LEVEL = logging.INFO
+
+class ILogger(Interface):
+    logger_name = schema.TextLine(
+        title = u'A Python logger name')
+    logger_filename = schema.TextLine(
+        title = u'A filename for the log file to use (basename)')
+    logger = Attribute("Get a Python logger instance already set up")
+    def logger_setup(logger):
+        """Setup a logger.
+
+        `logger` is an instance of :class:`logging.Logger`.
+        """
+
+    def logger_get_logfile_path():
+        """Get path to logfile used.
+
+        Return `None` if the file path cannot be computed.
+        """
+
+    def logger_get_logdir():
+        """Get the directory of the logfile.
+
+        Return `None` if the directory path cannot be computed.
+        """
+
+class ContextFilter(logging.Filter):
+    """
+    This is a filter which injects contextual information into the log.
+
+    """
+
+    def filter(self, record):
+        user = get_current_principal()
+        record.user = getattr(user, 'id', 'system')
+        return True
+
+class Logger(object):
+    """Mixin-class that for logging support.
+
+    Classes that (also) inherit from this class provide support for
+    logging with waeup sites.
+
+    By default a `logger` attribute is provided which returns a
+    regular Python logger. This logger has already registered a file
+    rotating log handler that writes log messages to a file `main.log`
+    in datacenters ``log/`` directory. This is the main log file also
+    used by other components. Therefore you can pick another filename
+    by setting the `logger_filename` attribute.
+
+    All methods and attributes of this mix-in start with ``logger_``
+    in order not to interfere with already existing names of a class.
+
+    Method names do not follow the usual Zope habit (CamelCase) but
+    PEP8 convention (lower_case_with_underscores).
+    """
+
+    #: The filename to use when logging.
+    logger_filename = 'main.log'
+
+    #: The Python logger name used when
+    #: logging. ``'waeup.kofa.${sitename}'`` by default. You can use the
+    #: ``${sitename}`` placeholder in that string, which will be
+    #: replaced by the actual used site name.
+    logger_name = 'waeup.kofa.${sitename}'
+
+    #: The format to use when logging.
+    logger_format_str = '%(asctime)s - %(levelname)s - %(user)s - %(message)s'
+
+    implements(ILogger)
+
+    @property
+    def logger(self):
+        """Get a logger instance.
+
+        Returns a standard logger object as provided by :mod:`logging`
+        module from the standard library.
+
+        Other components can use this logger to perform log entries
+        into the logfile of this component.
+
+        The logger is initialized the first time it is called.
+
+        The logger is only available when used inside a site.
+
+        .. note:: The logger default level is
+                  :data:`logging.WARN`. Use
+                  :meth:`logging.Logger.setLevel` to set a different level.
+        """
+        site = grok.getSite()
+        sitename = getattr(site, '__name__', None)
+        loggername = Template(self.logger_name).substitute(
+            dict(sitename='%s' % sitename))
+        logger = logging.getLogger(loggername)
+        if site is None or sitename is None:
+            # Site not added to ZODB yet. Log to commandline
+            return logger
+        if len(logger.handlers) != 1:
+            handlers = [x for x in logger.handlers]
+            for handler in handlers:
+                handler.flush()
+                handler.close()
+                logger.removeHandler(handler)
+            logger = self.logger_setup(logger)
+        if logger is None:
+            # It might happen, that we have no logger now.
+            logger = logging.getLogger(loggername)
+        return logger
+
+    def logger_setup(self, logger):
+        """Setup logger.
+
+        The logfile will be stored in the datacenter logs/ dir.
+        """
+        filename = self.logger_get_logfile_path()
+        if filename is None:
+            return
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            site = grok.getSite()
+            collector.registerLogger(site, self)
+
+        # Create a rotating file handler logger.
+        handler = WatchedFileHandler(
+            filename, encoding='utf-8')
+        handler.setLevel(LEVEL)
+        formatter = logging.Formatter(self.logger_format_str)
+        handler.setFormatter(formatter)
+
+        # Don't send log msgs to ancestors. This stops displaying
+        # logmessages on the commandline.
+        logger.propagate = False
+        logger.addHandler(handler)
+        logger.setLevel(LEVEL)
+
+        flt = ContextFilter()
+        logger.addFilter(flt)
+        return logger
+
+    def logger_get_logfile_path(self):
+        """Get the path to the logfile used.
+
+        Returns the path to a file in local sites datacenter ``log/``
+        directory (dependent on :meth:`logger_get_logdir`) and with
+        :attr:`logger_filename` as basename.
+
+        Override this method if you want a complete different
+        computation of the logfile path. If you only want a different
+        logfile name, set :attr:`logger_filename`. If you only want a
+        different path to the logfile override
+        :meth:`logger_get_logdir` instead.
+
+        Returns ``None`` if no logdir can be fetched.
+
+        .. note:: creates the logfile dir if it does not exist.
+
+        """
+        logdir = self.logger_get_logdir()
+        if logdir is None:
+            return None
+        return os.path.join(logdir, self.logger_filename)
+
+    def logger_get_logdir(self):
+        """Get log dir where logfile should be put.
+
+        Returns the path to the logfile directory. If no site is set,
+        ``None`` is returned. The same applies, if the site has no
+        datacenter.
+
+        If the dir dies not exist already it will be created. Only the
+        last part of the directory path will be created.
+        """
+        site = grok.getSite()
+        if site is None:
+            return None
+        datacenter = site.get('datacenter', None)
+        if datacenter is None:
+            return None
+        logdir = os.path.join(datacenter.storage, 'logs')
+        if not os.path.exists(logdir):
+            os.mkdir(logdir)
+        return logdir
+
+    def logger_logfile_changed(self):
+        """React on logfile location change.
+
+        If the logfile changed, we can set a different logfile. While
+        changing the logfile is a rather critical operation you might
+        not do often in production use, we have to cope with that
+        especially in tests.
+
+        What this method does by default (unless you override it):
+
+        - It fetches the current logger and
+
+          - Removes flushes, closes, and removes all handlers
+
+          - Sets up new handler(s).
+
+        All this, of course, requires to be 'in a site'.
+
+        Use this method to handle moves of datacenters, for instance
+        by writing an appropriate event handler.
+        """
+        logger = self.logger
+        self.logger_shutdown()
+        self.logger_setup(logger)
+        return
+
+    def logger_shutdown(self):
+        """Remove all specific logger setup.
+        """
+        logger = self.logger
+        handlers = [x for x in logger.handlers]
+        for handler in handlers:
+            handler.flush()
+            handler.close()
+            logger.removeHandler(handler)
+        collector = queryUtility(ILoggerCollector)
+        if collector is not None:
+            collector.unregisterLogger(grok.getSite(), self)
+        return
+
+
+class LoggerCollector(dict, grok.GlobalUtility):
+    """A global utility providing `ILoggerCollector`.
+
+    A logging collector collects logging components. This helps to
+    inform them when a logfile location changes.
+
+    Logging components are registered per site they belong to.
+    """
+
+    implements(ILoggerCollector)
+
+    def getLoggers(self, site):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return []
+        if name not in self.keys():
+            return []
+        return self[name]
+
+    def registerLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None:
+            return
+        if not name in self.keys():
+            # new component
+            self[name] = []
+        if logging_component in self[name]:
+            # already registered
+            return
+        self[name].append(logging_component)
+        return
+
+    def unregisterLogger(self, site, logging_component):
+        name = getattr(site, '__name__', None)
+        if name is None or name not in self.keys():
+            return
+        if logging_component not in self[name]:
+            return
+        self[name].remove(logging_component)
+        return
+
+@grok.subscribe(IDataCenter, IDataCenterStorageMovedEvent)
+def handle_datacenter_storage_moved(obj, event):
+    """Event handler, in case datacenter storage moves.
+
+    By default all our logfiles (yes, we produce a whole bunch of it)
+    are located in a ``log/`` dir of a local datacenter, the
+    datacenter 'storage'. If this path changes because the datacenter
+    is moved an appropriate event is triggered and we can react.
+
+    Via the global ILoggerCollector utility, a small piece that allows
+    self-registering of logging components, we can lookup components
+    whose logfile path has to be set up anew.
+
+    Each component we call has to provide ILogger or, more specific,
+    the :meth:`logger_logfile_changed` method of this interface.
+    """
+    site = grok.getSite()
+    if site is None:
+        return
+    collector = queryUtility(ILoggerCollector)
+    loggers = collector.getLoggers(site)
+    for logger in loggers:
+        if hasattr(logger, 'logger_logfile_changed'):
+            logger.logger_logfile_changed()
+    return
+
+from waeup.kofa.interfaces import IUniversity
+@grok.subscribe(IUniversity, grok.IObjectRemovedEvent)
+def handle_site_removed(obj, event):
+    collector = queryUtility(ILoggerCollector)
+    name = getattr(obj, '__name__', None)
+    if name is None:
+        return
+    if name not in collector.keys():
+        return
+    del collector[name]
+    return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/overrides.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/overrides.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/overrides.zcml	(revision 16045)
@@ -0,0 +1,11 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <adapter
+      for="zope.schema.interfaces.IBool
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory=".converters.ExtendedCheckBoxWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_meminfo
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_meminfo	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_meminfo	(revision 16045)
@@ -0,0 +1,42 @@
+MemTotal:       16345792 kB
+MemFree:        13431496 kB
+Buffers:          218528 kB
+Cached:          1013816 kB
+SwapCached:            0 kB
+Active:          1661796 kB
+Inactive:         839776 kB
+Active(anon):    1270232 kB
+Inactive(anon):     2292 kB
+Active(file):     391564 kB
+Inactive(file):   837484 kB
+Unevictable:          16 kB
+Mlocked:              16 kB
+SwapTotal:       7999484 kB
+SwapFree:        7999484 kB
+Dirty:                40 kB
+Writeback:             0 kB
+AnonPages:       1269248 kB
+Mapped:           236544 kB
+Shmem:              3296 kB
+Slab:             131344 kB
+SReclaimable:      95972 kB
+SUnreclaim:        35372 kB
+KernelStack:        4376 kB
+PageTables:        31384 kB
+NFS_Unstable:          0 kB
+Bounce:                0 kB
+WritebackTmp:          0 kB
+CommitLimit:    16172380 kB
+Committed_AS:    4243140 kB
+VmallocTotal:   34359738367 kB
+VmallocUsed:      610564 kB
+VmallocChunk:   34359120892 kB
+HardwareCorrupted:     0 kB
+AnonHugePages:         0 kB
+HugePages_Total:       0
+HugePages_Free:        0
+HugePages_Rsvd:        0
+HugePages_Surp:        0
+Hugepagesize:       2048 kB
+DirectMap4k:      167936 kB
+DirectMap2M:    16525312 kB
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_response.html
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_response.html	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/sample_response.html	(revision 16045)
@@ -0,0 +1,45 @@
+HTTP/1.1 200 OK
+Date: Thu, 19 Sep 2019 14:12:36 GMT
+Server: WildFly/10
+X-Frame-Options: SAMEORIGIN
+X-Powered-By: Undertow/1
+X-Powered-By: JSP/2.3
+Content-Type: text/html;charset=UTF-8
+Content-Length: 1653
+Set-Cookie: JSESSIONID=-JcbFOsUjSI_yJpRohp88D4vnTTSDYshK1ApgThJ.etz-firstest-sv; path=/WebKonnect
+
+
+
+
+
+
+
+
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+    "http://www.w3.org/TR/html4/loose.dtd">
+
+<script language='javascript'>
+var spanId = document.getElementById("message_container");
+spanId.textContent = 'Redirecting...';</script>
+<form method="GET" id="redirect_form" name="redirect_form" action="http://localhost:81/projects/Webconnect/response.php" target="_top">
+<input type="hidden" name = "LOGO_URL" value="http://localhost:81/projects/Webconnect/images/elogo.fw.png">
+<input type="hidden" name = "RESPONSE_URL" value="http://localhost:81/projects/Webconnect/response.php">
+<input type="hidden" name = "CURRENCY_CODE" value="NGN">
+<input type="hidden" name = "TERMINAL_ID" value="0000000001">
+<input type="hidden" name = "TRANSACTION_ID" value="etz1568638104web">
+<input type="hidden" name = "AMOUNT" value="100">
+<input type="hidden" name = "DESCRIPTION" value="Payment Description">
+<input type="hidden" name = "CHECKSUM" value="5be04064f4bb250f73650059a8e921cc">
+<input type="hidden" name = "MERCHANT_CODE" value="0339990001">
+<input type="hidden" name = "EMAIL" value="xyz@yahoo.com">
+<input type="hidden" name = "SUCCESS" value="0">
+<input type="hidden" name = "FINAL_CHECKSUM" value="FD67A4CCC39E2C8DEEEC78D6C64A61FE">
+<input type="hidden" name = "STATUS_REASON" value="Approved">
+<input type="hidden" name = "TRANS_NUM" value="01ESA20190916134824YA3YJ8">
+<input type="hidden" name = "CARD_NO" value="506066XXXXXXXXX6666">
+<input type="hidden" name = "CARD_TYPE" value="Verve">
+</form>
+<script language='javascript'>
+var fom = document.forms["redirect_form"];
+fom.submit();</script>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_batching.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_batching.py	(revision 16045)
@@ -0,0 +1,635 @@
+## $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
+##
+import datetime
+import doctest
+import logging
+import os
+import shutil
+import tempfile
+import unittest
+from zc.async.interfaces import IJob
+from zope import schema
+from zope.component import provideUtility, getGlobalSiteManager, getUtility
+from zope.component.factory import Factory
+from zope.component.hooks import clearSite, setSite
+from zope.component.interfaces import IFactory
+from zope.interface import Interface, implements, verify
+from waeup.kofa.app import University
+from waeup.kofa.interfaces import (
+    ICSVExporter, IBatchProcessor, IExportJobContainer, IJobManager,
+    IExportJob, IExportContainerFinder)
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, FakeJob, FakeJobManager)
+from waeup.kofa.utils.batching import (
+    ExporterBase, BatchProcessor, export_job, AsyncExportJob,
+    ExportJobContainer, VirtualExportJobContainer, ExportContainerFinder)
+
+optionflags = (
+    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
+
+
+class ICave(Interface):
+    """A cave."""
+    id_num = schema.TextLine(
+        title = u'internal id',
+        default = u'default',
+        required = True,
+        readonly = True,
+        )
+    name = schema.TextLine(
+        title = u'Cave name',
+        default = u'Unnamed',
+        required = True)
+    dinoports = schema.Int(
+        title = u'Number of DinoPorts (tm)',
+        required = False,
+        default = 1)
+    owner = schema.TextLine(
+        title = u'Owner name',
+        required = True,
+        missing_value = 'Fred Estates Inc.')
+    taxpayer = schema.Bool(
+        title = u'Payes taxes',
+        required = True,
+        default = False)
+
+class Cave(object):
+    implements(ICave)
+    def __init__(self, name=u'Unnamed', dinoports=2,
+                 owner='Fred Estates Inc.', taxpayer=False):
+        self.name = name
+        self.dinoports = 2
+        self.owner = owner
+        self.taxpayer = taxpayer
+
+stoneville = dict
+
+SAMPLE_DATA = """name,dinoports,owner,taxpayer
+Barneys Home,2,Barney,1
+Wilmas Asylum,1,Wilma,1
+Freds Dinoburgers,10,Fred,0
+Joeys Drive-in,110,Joey,0
+"""
+
+SAMPLE_FILTERED_DATA = """name,dinoports,owner,taxpayer
+Barneys Home,2,Barney,1
+Wilmas Asylum,1,Wilma,1
+"""
+
+class CaveProcessor(BatchProcessor):
+    util_name = 'caveprocessor'
+    name = 'Cave Processor'
+    iface = ICave
+    location_fields = ['name']
+    factory_name = 'Lovely Cave'
+
+    def parentsExist(self, row, site):
+        return True
+
+    def getParent(self, row, site):
+        return stoneville
+
+    def entryExists(self, row, site):
+        return row['name'] in stoneville.keys()
+
+    def getEntry(self, row, site):
+        if not self.entryExists(row, site):
+            return None
+        return stoneville[row['name']]
+
+    def delEntry(self, row, site):
+        del stoneville[row['name']]
+
+    def addEntry(self, obj, row, site):
+        stoneville[row['name']] = obj
+
+class BatchProcessorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setupLogger(self):
+
+        self.logger = logging.getLogger('stoneville')
+        self.logger.setLevel(logging.DEBUG)
+        self.logger.propagate = False
+        self.logfile = os.path.join(self.workdir, 'stoneville.log')
+        self.handler = logging.FileHandler(self.logfile, 'w')
+        self.logger.addHandler(self.handler)
+
+    def setUp(self):
+        global stoneville
+        super(BatchProcessorTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+        factory = Factory(Cave)
+        provideUtility(factory, IFactory, 'Lovely Cave')
+
+        # Provide sample data
+        self.newcomers_csv = os.path.join(self.workdir, 'newcomers.csv')
+        open(self.newcomers_csv, 'wb').write(SAMPLE_DATA)
+        self.setupLogger()
+        self.stoneville = stoneville
+        stoneville = dict()
+        self.resultpath = None
+        return
+
+    def tearDown(self):
+        super(BatchProcessorTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        self.logger.removeHandler(self.handler)
+        clearSite()
+        if not isinstance(self.resultpath, list):
+            self.resultpath = [self.resultpath]
+        for path in self.resultpath:
+            if not isinstance(path, basestring):
+                continue
+            if not os.path.isdir(path):
+                path = os.path.dirname(path)
+            if os.path.exists(path):
+                shutil.rmtree(path)
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = BatchProcessor()
+        verify.verifyClass(IBatchProcessor, BatchProcessor)
+        verify.verifyObject(IBatchProcessor, obj)
+        return
+
+    def test_import(self):
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num_succ, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        assert num_succ == 4
+        assert num_fail == 0
+        assert finished_path.endswith('/newcomers.finished.csv')
+        assert failed_path is None
+
+    def test_import_stoneville(self):
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num_succ, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        assert len(self.stoneville) == 4
+        self.assertEqual(
+            sorted(self.stoneville.keys()),
+            [u'Barneys Home', u'Freds Dinoburgers',
+             u'Joeys Drive-in', u'Wilmas Asylum'])
+
+    def test_import_correct_type(self):
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num_succ, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        assert isinstance(self.stoneville['Barneys Home'].dinoports, int)
+
+
+    def test_log(self):
+        """
+           >>> print log_contents
+           processed: /.../newcomers.csv, create mode, 4 lines (4 successful/ 0 failed), ... s (... s/item)
+
+        """
+        processor = CaveProcessor()
+        result = processor.doImport(
+            self.newcomers_csv,
+            ['name', 'dinoports', 'owner', 'taxpayer'],
+            mode='create', user='Bob', logger=self.logger)
+        num_succ, num_fail, finished_path, failed_path = result
+        self.resultpath = [finished_path, failed_path]
+        log_contents = open(self.logfile, 'rb').read()
+        doctest.run_docstring_examples(
+            self.test_log, locals(), False, 'test_log', None, optionflags)
+        return
+
+class ExporterBaseTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.workfile = os.path.join(self.workdir, 'testfile.csv')
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_iface(self):
+        # ExporterBase really implements the promised interface.
+        obj = ExporterBase()
+        verify.verifyClass(ICSVExporter, ExporterBase)
+        verify.verifyObject(ICSVExporter, obj)
+        return
+
+    def test_unimplemented(self):
+        # make sure the not implemented methods signal that.
+        exporter = ExporterBase()
+        self.assertRaises(NotImplementedError, exporter.export_all, None)
+        self.assertRaises(NotImplementedError, exporter.export, None)
+        return
+
+    def test_mangle_value(self):
+        # some basic types are mangled correctly
+        exporter = ExporterBase()
+        result1 = exporter.mangle_value(True, 'foo')
+        result2 = exporter.mangle_value(False, 'foo')
+        result3 = exporter.mangle_value('string', 'foo')
+        result4 = exporter.mangle_value(u'string', 'foo')
+        result5 = exporter.mangle_value(None, 'foo')
+        result6 = exporter.mangle_value(datetime.date(2012, 4, 1), 'foo')
+        result7 = exporter.mangle_value(
+            datetime.datetime(2012, 4, 1, 12, 1, 1), 'foo')
+        self.assertEqual(
+            (result1, result2, result3, result4, result5),
+            ('1', '0', u'string', u'string', ''))
+        self.assertEqual(type(result3), type('string'))
+        self.assertEqual(type(result4), type('string'))
+        # dates are formatted with trailing hash
+        self.assertEqual(result6, '2012-04-01#')
+        # datetimes are formatted as yyyy-mm-dd hh:mm:ss
+        self.assertEqual(result7, '2012-04-01 12:01:01#')
+        return
+
+    def test_get_csv_writer(self):
+        # we can get a CSV writer to a memory file
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        writer.writerow(dict(code='A', title='B', title_prefix='C'))
+        outfile.seek(0)
+        self.assertEqual(
+            outfile.read(),
+            'code,title,title_prefix\r\nA,B,C\r\n')
+        return
+
+    def test_get_csv_writer_with_file(self):
+        # we can get CSV writer that writes to a real file
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        writer.writerow(dict(code='A', title='B', title_prefix='C'))
+        outfile.close()
+        resultfile = open(self.workfile, 'rb')
+        self.assertEqual(
+            resultfile.read(),
+            'code,title,title_prefix\r\nA,B,C\r\n')
+        return
+
+    def test_write_item(self):
+        # we can write items to opened exporter files.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        class Sample(object):
+            code = 'A'
+            title = u'B'
+            title_prefix = True
+        exporter.write_item(Sample(), writer)
+        outfile.seek(0)
+        self.assertEqual(
+            outfile.read(),
+            'code,title,title_prefix\r\nA,B,1\r\n')
+        return
+
+    def test_close_outfile(self):
+        # exporters can help to close outfiles.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer()
+        result = exporter.close_outfile(None, outfile)
+        self.assertEqual(result, 'code,title,title_prefix\r\n')
+        return
+
+    def test_close_outfile_real(self):
+        # we can also close outfiles in real files.
+        exporter = ExporterBase()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        result = exporter.close_outfile(self.workfile, outfile)
+        self.assertEqual(result, None)
+        return
+
+    def test_export_filtered(self):
+        # we can pass in positional and keyword args
+        exporter = ExporterBase()
+        self.assertRaises(NotImplementedError, exporter.export_filtered,
+                          'foo', bar='bar')
+
+    def test_export_selected(self):
+        # we can pass in a list of identifiers
+        exporter = ExporterBase()
+        self.assertRaises(NotImplementedError, exporter.export_selected,
+                          'foo', selected=[])
+        return
+
+class CaveExporter(ExporterBase):
+    # A minimal fake exporter suitable to be called by export_jobs
+    fields = ('name', 'dinoports', 'owner', 'taxpayer')
+    title = u'Dummy cave exporter'
+
+    def export_all(self, site, filepath=None):
+        if filepath is None:
+            return SAMPLE_DATA
+        open(filepath, 'wb').write(SAMPLE_DATA)
+        return
+
+    def export_filtered(self, site, filepath=None, foo=None, bar=None):
+        if foo or bar:
+            open(filepath, 'wb').write(SAMPLE_FILTERED_DATA)
+            return
+        self.export_all(site, filepath=filepath)
+        return
+
+    def export_selected(self, site, filepath=None, selected=None):
+        if selected:
+            open(filepath, 'wb').write(SAMPLE_FILTERED_DATA)
+            return
+        self.export_all(site, filepath=filepath)
+        return
+
+class ExportJobTests(unittest.TestCase):
+    # Test asynchronous export functionality (simple cases)
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.exporter = CaveExporter()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.exporter, ICSVExporter, name='cave_exporter')
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.exporter)
+
+    def test_export_job_func(self):
+        # the export_job func does really export data...
+        result_path = export_job(None, 'cave_exporter')
+        self.assertTrue(os.path.isfile(result_path))
+        contents = open(result_path, 'rb').read()
+        shutil.rmtree(os.path.dirname(result_path))
+        self.assertEqual(contents, SAMPLE_DATA)
+        return
+
+    def test_export_job_interfaces(self):
+        # the AsyncExportJob implements promised interfaces correctly...
+        job = AsyncExportJob(None, None)
+        verify.verifyClass(IJob, AsyncExportJob)
+        verify.verifyObject(IJob, job)
+        verify.verifyClass(IExportJob, AsyncExportJob)
+        verify.verifyObject(IExportJob, job)
+        return
+
+    def test_export_job_with_args(self):
+        # we export filtered sets
+        result_path = export_job(None, 'cave_exporter', foo='foo')
+        contents = open(result_path, 'rb').read()
+        shutil.rmtree(os.path.dirname(result_path))
+        self.assertEqual(contents, SAMPLE_FILTERED_DATA)
+        return
+
+    def test_export_job_selected(self):
+        # we export subsets
+        result_path = export_job(None, 'cave_exporter', selected=['foo'])
+        contents = open(result_path, 'rb').read()
+        shutil.rmtree(os.path.dirname(result_path))
+        self.assertEqual(contents, SAMPLE_FILTERED_DATA)
+        return
+
+
+class FakeJobWithResult(FakeJob):
+
+    def __init__(self):
+        self.dir_path = tempfile.mkdtemp()
+        self.result = os.path.join(self.dir_path, 'fake.csv')
+        open(self.result, 'wb').write('a fake result')
+        return
+
+class ExportJobContainerTests(unittest.TestCase):
+    # Test ExportJobContainer
+
+    TestedClass = ExportJobContainer
+
+    def setUp(self):
+        # register a suitable ICSVExporter as named utility
+        self.exporter = CaveExporter()
+        self.job_manager = FakeJobManager()
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(
+            self.exporter, ICSVExporter, name='cave_exporter')
+        self.gsm.registerUtility(
+            self.job_manager, IJobManager)
+
+    def tearDown(self):
+        self.gsm.unregisterUtility(self.exporter)
+        self.gsm.unregisterUtility(self.job_manager, IJobManager)
+
+    def test_export_job_interfaces(self):
+        # the ExportJobContainer implements promised interfaces correctly...
+        container = self.TestedClass()
+        verify.verifyClass(IExportJobContainer, self.TestedClass)
+        verify.verifyObject(IExportJobContainer, container)
+        return
+
+    def test_start_export_job(self):
+        # we can start jobs
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        result = self.job_manager._jobs.values()[0]
+        self.assertTrue(IJob.providedBy(result))
+        self.assertEqual(
+            container.running_exports,
+            [('1', 'cave_exporter', 'bob')]
+            )
+        return
+
+    def test_get_running_export_jobs_all(self):
+        # we can get export jobs of all users
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'alice')
+        result = container.get_running_export_jobs()
+        self.assertEqual(
+            result,
+            [('1', 'cave_exporter', 'bob'),
+             ('2', 'cave_exporter', 'alice')]
+            )
+        return
+
+    def test_get_running_export_jobs_user(self):
+        # we can get the export jobs running for a certain user
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'alice')
+        result1 = container.get_running_export_jobs(user_id='alice')
+        result2 = container.get_running_export_jobs(user_id='foo')
+        self.assertEqual(
+            result1, [('2', 'cave_exporter', 'alice')])
+        self.assertEqual(
+            result2, [])
+        return
+
+    def test_get_running_export_jobs_only_if_exist(self):
+        # we get only jobs that are accessible through the job manager...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'bob')
+        self.assertTrue(
+            ('2', 'cave_exporter', 'bob') in container.running_exports)
+        # we remove the second entry from job manager
+        del self.job_manager._jobs['2']
+        result = container.get_running_export_jobs(user_id='bob')
+        self.assertEqual(
+            result, [('1', 'cave_exporter', 'bob')])
+        self.assertTrue(
+            ('2', 'cave_exporter', 'bob') not in container.running_exports)
+        return
+
+    def test_get_export_job_status(self):
+        # we can get the stati of jobs...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'alice')
+        container.start_export_job('cave_exporter', 'bob')
+        container.start_export_job('cave_exporter', 'bob')
+        result = container.get_export_jobs_status(user_id='bob')
+        # we'll get the raw value, a translation and the title of the
+        # exporter
+        self.assertEqual(
+            result,
+            [('new', u'new', u'Dummy cave exporter'),
+             ('completed', u'completed', u'Dummy cave exporter')]
+            )
+        return
+
+    def test_delete_export_entry(self):
+        # we can remove export entries in local lists and the job
+        # manager as well...
+        container = self.TestedClass()
+        container.start_export_job('cave_exporter', 'bob')
+        entry = container.running_exports[0]
+        container.delete_export_entry(entry)
+        # both, running_exports list and job manager are empty now
+        self.assertEqual(
+            container.running_exports, [])
+        self.assertEqual(
+            self.job_manager._jobs, {})
+        return
+
+    def test_delete_export_entry_remove_file(self):
+        # any result files of exports are deleted as well
+        container = self.TestedClass()
+        entry = ('4', 'cave_exporter', 'bob')
+        container.running_exports = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        self.assertTrue(os.path.isfile(fake_job.result))
+        container.delete_export_entry(entry)
+        self.assertTrue(not os.path.exists(fake_job.result))
+        return
+
+    def test_entry_from_job_id(self):
+        # we can get an entry for a job_id if the id exists
+        container = self.TestedClass()
+        entry = ('4', 'cave_exporter', 'bob')
+        container.running_exports = [entry]
+        fake_job = FakeJobWithResult()
+        self.job_manager._jobs['4'] = fake_job
+        result1 = container.entry_from_job_id(None)
+        result2 = container.entry_from_job_id('4')
+        result3 = container.entry_from_job_id('23')
+        self.assertEqual(result1, None)
+        self.assertEqual(result2, ('4', 'cave_exporter', 'bob'))
+        self.assertEqual(result3, None)
+        shutil.rmtree(fake_job.dir_path)
+        return
+
+class VirtualExportJobContainerTests(ExportJobContainerTests):
+    # VirtualExportJobContainers should provide the
+    # same functionality as regular ones.
+
+    TestedClass = VirtualExportJobContainer
+
+    def setUp(self):
+        super(VirtualExportJobContainerTests, self).setUp()
+        self.root_job_container = ExportJobContainer()
+        def fake_finder():
+            return self.root_job_container
+        self.gsm = getGlobalSiteManager()
+        self.gsm.registerUtility(fake_finder, IExportContainerFinder)
+        return
+
+class ExportContainerFinderTests(FunctionalTestCase):
+    # Tests for export container finder.
+
+    layer = FunctionalLayer
+
+    def test_get_finder_as_util(self):
+        # we can get a finder by utility lookup
+        finder = getUtility(IExportContainerFinder)
+        self.assertTrue(finder is not None)
+        self.assertEqual(
+            IExportContainerFinder.providedBy(finder),
+            True)
+        return
+
+    def test_iface(self):
+        # the finder complies with the promised interface
+        finder = ExportContainerFinder()
+        verify.verifyClass(IExportContainerFinder,
+                           ExportContainerFinder)
+        verify.verifyObject(IExportContainerFinder, finder)
+        return
+
+    def test_no_site(self):
+        # a finder returns None if no site is available
+        finder = ExportContainerFinder()
+        self.assertEqual(
+            finder(), None)
+        return
+
+    def test_active_site(self):
+        # we get the datafinder if one is installed and site set
+        self.getRootFolder()['app'] = University()
+        finder = getUtility(IExportContainerFinder)
+        setSite(self.getRootFolder()['app'])
+        container = finder()
+        self.assertTrue(container is not None)
+        return
+
+    def test_broken_site(self):
+        # if the current site has no ExportContainer, we get None
+        self.getRootFolder()['app'] = University()
+        app = self.getRootFolder()['app']
+        del app['datacenter'] # datacenter _is_ the export container
+        setSite(app)
+        finder = getUtility(IExportContainerFinder)
+        container = finder()
+        self.assertTrue(container is None)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_browser.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_browser.py	(revision 16045)
@@ -0,0 +1,143 @@
+## $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)
+        self.browser.open('http://localhost/app/reindex?ctlg=students')
+        self.assertTrue('1 students re-indexed' in self.browser.contents)
+        cat.clear()
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),0)
+        self.browser.open('http://localhost/app/reindex?ctlg=students')
+        # No docids found, the new reindex method does only reindex existing
+        # catalog entries.
+        self.assertTrue('0 students re-indexed' in self.browser.contents)
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),0)
+
+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-py3/src/waeup/kofa/utils/tests/test_converters.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_converters.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_converters.py	(revision 16045)
@@ -0,0 +1,450 @@
+## $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
+##
+"""
+Tests for converterts.
+"""
+import datetime
+import shutil
+import tempfile
+import unittest
+from zope import schema
+from zope.component import provideUtility
+from zope.component.factory import Factory
+from zope.component.hooks import clearSite
+from zope.component.interfaces import IFactory
+from zope.formlib import form
+from zope.interface import (
+    Interface, implements, invariant, Invalid, implementedBy, verify)
+
+from waeup.kofa.app import University
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+from waeup.kofa.interfaces import (
+    SimpleKofaVocabulary, SubjectSource, GradeSource, IFieldConverter,
+    DELETION_MARKER, IGNORE_MARKER)
+from waeup.kofa.schoolgrades import ResultEntryField
+from waeup.kofa.utils.converters import (
+    IObjectConverter, DefaultFieldConverter, ListFieldConverter,
+    PhoneNumberFieldConverter, ResultEntryConverter, DefaultObjectConverter)
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+colors = SimpleKofaVocabulary(
+    ('Red', u'red'),
+    ('Green', u'green'),
+    ('Blue', u'blue'),
+    )
+car_nums = SimpleKofaVocabulary(
+    ('None', 0),
+    ('One', 1),
+    ('Two', 2),
+    ('Three', 3),
+    )
+
+
+class IContact(Interface):
+    """Sample interface for sample content type used here in tests.
+    """
+    name = schema.TextLine(
+        title=u'Name',
+        default=u'Manfred',
+        readonly=True,
+        )
+    age = schema.Int(
+        title=u'Age',
+        default=23,
+        required=True,
+        )
+    city = schema.TextLine(
+        title=u'City',
+        required=True,
+        )
+    vip = schema.Bool(
+        title=u'Celebrity',
+        default=False,
+        required=True,
+        )
+    birthday = schema.Date(
+        title=u'Birthday',
+        default=None,
+        )
+    fav_color = schema.Choice(
+        title=u'Favourite color',
+        default=u'red',
+        vocabulary=colors,
+        )
+    num_cars = schema.Choice(
+        title=u'Number of cars owned',
+        default=None,
+        vocabulary=car_nums,
+        )
+    grades = schema.List(
+        title=u'School Grades',
+        value_type=ResultEntryField(),
+        required=True,
+        defaultFactory=list
+        )
+    fav_colors = schema.List(
+        title=u'Favourite colors',
+        value_type=schema.Choice(
+            vocabulary=colors
+            ),
+        required=True,
+        defaultFactory=list
+        )
+    friends = schema.List(
+        title=u'Friends',
+        value_type=schema.TextLine(
+            title=u'Name',
+            )
+        )
+
+    @invariant
+    def kevinIsYoung(contact):
+        if contact.age > 16 and contact.name == 'Kevin':
+            raise Invalid('Kevins are age 16 or below.')
+
+
+class Contact(object):
+    """Sample content type.
+    """
+    implements(IContact)
+Contact = attrs_to_fields(Contact)
+
+form_fields_select = form.Fields(IContact).select('name', 'vip')
+form_fields_omit = form.Fields(IContact).omit('name', 'vip')
+
+
+class TestContacts(unittest.TestCase):
+    # make sure the Contact class inhere works as expected
+
+    def test_default_lists_not_shared(self):
+        # 'grades' is a Contact property with empty list as default.
+        # Ensure lists are unique if set as default this test fails if
+        # `default` is set to empty list in interface.
+        c1 = Contact()
+        c2 = Contact()
+        self.assertTrue(c1.grades is not c2.grades)
+
+
+class FieldConverterTests(unittest.TestCase):
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj1 = DefaultFieldConverter(None)
+        obj2 = ListFieldConverter(None)
+        obj3 = PhoneNumberFieldConverter(None)
+        obj4 = ResultEntryConverter(None)
+        verify.verifyObject(IFieldConverter, obj1)
+        verify.verifyObject(IFieldConverter, obj2)
+        verify.verifyObject(IFieldConverter, obj3)
+        verify.verifyObject(IFieldConverter, obj4)
+        verify.verifyClass(IFieldConverter, DefaultFieldConverter)
+        verify.verifyClass(IFieldConverter, ListFieldConverter)
+        verify.verifyClass(IFieldConverter, PhoneNumberFieldConverter)
+        verify.verifyClass(IFieldConverter, ResultEntryConverter)
+        return
+
+
+class ConverterTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ConverterTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+
+        # Create a factory for contacts and register it as global utility
+        factory = Factory(Contact)
+        provideUtility(factory, IFactory, 'contact')
+        return
+
+    def tearDown(self):
+        super(ConverterTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = DefaultObjectConverter(IContact)
+        verify.verifyObject(IObjectConverter, obj)
+        verify.verifyClass(IObjectConverter, DefaultObjectConverter)
+        return
+
+    def test_valid_data(self):
+        contact = Contact()
+        contact.age = 33
+        input_data = dict(name='Rudi', age='99')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        assert data['name'] == 'Rudi'
+        assert data['age'] == 99
+        return
+
+    def test_bool(self):
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='on')
+        input_data2 = dict(vip='')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values1(self):
+         # We accept 'true', 'True', 'tRuE', 'faLSE' and similar.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='True')
+        input_data2 = dict(vip='false')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values2(self):
+        # We accept '1' and '0' as bool values.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='1')
+        input_data2 = dict(vip='0')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_bool_nonstandard_values3(self):
+        # We accept 'yEs', 'no' and similar as bool values.
+        contact1 = Contact()
+        contact2 = Contact()
+        input_data1 = dict(vip='Yes')
+        input_data2 = dict(vip='no')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err1, inv_err1, data1 = converter.fromStringDict(
+            input_data1, contact1)
+        err2, inv_err2, data2 = converter.fromStringDict(
+            input_data2, contact2)
+        assert data1['vip'] is True
+        assert data2['vip'] is False
+
+    def test_int(self):
+        contact = Contact()
+        input_data = dict(age='99')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        assert data['age'] == 99
+        return
+
+    def test_int_invalid(self):
+        contact = Contact()
+        input_data = dict(age='sweet sixteen')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, new_contact = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(err, [('age', u'Invalid integer data')])
+        return
+
+    def test_textline(self):
+        contact = Contact()
+        input_data = dict(name='Rudi')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(data['name'], u'Rudi')
+        assert isinstance(data['name'], unicode)
+        return
+
+    def test_invariant(self):
+        contact = Contact()
+        input_data = dict(name='Kevin', age='22')
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, new_contact = converter.fromStringDict(
+            input_data, contact)
+        self.assertEqual(inv_err, ['Kevins are age 16 or below.'])
+        return
+
+    def test_date(self):
+        contact = Contact()
+        converter = IObjectConverter(IContact)  # a converter to IContact
+
+        # The input format for dates: YYYY-MM-DD
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-12-23'), contact)
+        assert data['birthday'] == datetime.date(1945, 12, 23)
+        assert isinstance(data['birthday'], datetime.date)
+
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-23-12'), contact)
+        #assert data['birthday'] == datetime.date(1945, 12, 23)
+        assert err[0][1] == 'Invalid datetime data'
+
+        # '08' is not interpreted as octal number
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-12-08'), contact)
+        assert data['birthday'] == datetime.date(1945, 12, 8)
+        return
+
+    def test_date_invalid(self):
+        contact = Contact()
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='not-a-date'), contact)
+        self.assertEqual(err, [('birthday', u'Invalid datetime data')])
+
+    def test_inject_formfields_select(self):
+        # We can use our own formfields and select only a subset of fields
+        contact = Contact()
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_select)
+        self.assertEqual(data['name'], 'Bruno')
+        assert 'age' not in data.keys()
+        assert data['vip'] is True
+        return
+
+    def test_inject_formfields_omit(self):
+        # We can use our own formfields and omit some fields
+        contact = Contact()
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_omit)
+        self.assertEqual(data['age'], 99)
+        assert 'name' not in data.keys()
+        assert 'vip' not in data.keys()
+        return
+
+    def test_factory(self):
+        # We can use factories to convert values
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        # pass string ``contact`` instead of a real object
+        err, inv_err, data = converter.fromStringDict(
+            dict(name='Gabi', age='23'), 'contact')
+        self.assertEqual(data['age'], 23)
+        self.assertEqual(data['name'], u'Gabi')
+        return
+
+    def test_choice_vocab(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='blue'), 'contact')
+        assert data['fav_color'] == u'blue'
+        assert isinstance(data['fav_color'], unicode)
+        return
+
+    def test_choice_vocab_invalid_value(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='magenta'), 'contact')
+        self.assertEqual(err, [('fav_color', u'Invalid value')])
+        assert 'fav_color' not in data.keys()
+        return
+
+    def test_non_string_choice(self):
+        # We can handle vocabs with non-string values
+        converter = IObjectConverter(IContact)  # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(num_cars='1'), 'contact')
+        assert data['num_cars'] == 1
+        return
+
+    def test_list_of_textlines(self):
+        # We can convert lists of text lines
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"friends": "['Fred', 'Wilma']"}, 'contact')
+        self.assertEqual(
+            data, {'friends': ['Fred', 'Wilma']})
+        return
+
+    def test_list_of_resultentries(self):
+        # We can handle lists of result entries
+        converter = IObjectConverter(IContact)
+        # get currently valid values
+        s_src, g_src = SubjectSource(), GradeSource()
+        s_val1, s_val2 = list(s_src.factory.getValues())[0:2]
+        g_val1, g_val2 = list(g_src.factory.getValues())[0:2]
+        req_string = u"[('%s', '%s'), ('%s', '%s')]" % (
+            s_val1, g_val1, s_val2, g_val2)
+        err, inv_err, data = converter.fromStringDict(
+            {"grades": req_string,
+             },
+            'contact')
+        result_grades = data['grades']
+        self.assertTrue(isinstance(result_grades, list))
+        self.assertEqual(len(result_grades), 2)
+        self.assertEqual(result_grades[0].subject, s_val1)
+        self.assertEqual(result_grades[0].grade, g_val1)
+        return
+
+    def test_list_of_choices(self):
+        # We cannot handle lists of choices because we are using
+        # a widget which is not yet supported.
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"fav_colors": "['red', 'green']"}, 'contact')
+        self.assertEqual(
+            data, {})
+        return
+
+    def test_ignore_values(self):
+        # in update mode we ignore marked values
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"friends": IGNORE_MARKER},
+            Contact(),
+            mode='update')
+        # the ignored key/value are not part of the result
+        self.assertEqual(data, {})
+        return
+
+    def test_delete_values(self):
+        # in update mode we delete values marked accordingly
+        # 'deleting' means setting to missing_value or to default if required.
+        converter = IObjectConverter(IContact)
+        err, inv_err, data = converter.fromStringDict(
+            {"grades": DELETION_MARKER,
+             "friends": DELETION_MARKER},
+            'contact', mode='update')
+        # grades are about to be set to default, friends to None
+        self.assertEqual(data, {'grades': [], 'friends': None})
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_doctests.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_doctests.py	(revision 16045)
@@ -0,0 +1,24 @@
+## $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
+##
+"""Register doctests from utils subpackage.
+"""
+from waeup.kofa.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite(['doctests/batchprocessing.txt'])
+    return suite
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_helpers.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_helpers.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_helpers.py	(revision 16045)
@@ -0,0 +1,659 @@
+# -*- coding: utf-8 -*-
+
+## $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
+##
+
+import datetime
+import os
+import pytz
+import shutil
+import tempfile
+import unittest
+import doctest
+from cStringIO import StringIO
+from zope import schema
+from zope.interface import Interface, Attribute, implements, implementer
+from zope.security.testing import Principal, Participation
+from zope.security.management import newInteraction, endInteraction
+from waeup.kofa.utils import helpers
+
+class IFakeObject(Interface):
+    """Some marker interface."""
+
+class FakeObject(object):
+    implements(IFakeObject)
+
+class SimpleHelpersTestCase(unittest.TestCase):
+    # Tests for simple functions in `helpers`.
+    def test_product(self):
+        # the product will return zero without input
+        result1 = helpers.product([])
+        result2 = helpers.product([1,2,3])
+        result3 = helpers.product([], start=5)
+        result4 = helpers.product([1,2,3], start=5)
+        self.assertEqual(result1, 0)
+        self.assertEqual(result2, 6)
+        self.assertEqual(result3, 0)
+        self.assertEqual(result4, 30)
+        return
+
+    def test_attrs_to_fields_properties(self):
+        # we can omit single fields in order to retrieve properties
+        class IMyInterface(Interface):
+            attr1 = schema.Int(
+                title = u'Attribute 1', readonly = True, default = 110,
+                )
+
+        @helpers.attrs_to_fields
+        @implementer(IMyInterface)
+        class MyClass1(object):
+            @property
+            def attr1(self):
+                return 42
+
+        @implementer(IMyInterface)
+        class MyClass2(object):
+            @property
+            def attr1(self):
+                return 42
+        MyClass2 = helpers.attrs_to_fields(MyClass2, omit=['attr1'])
+
+        obj1 = MyClass1()
+        obj2 = MyClass2()
+
+        self.assertEqual(obj1.attr1, 110)
+        self.assertEqual(obj2.attr1, 42)
+        return
+
+
+class RemoveFileOrDirectoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.dirpath = tempfile.mkdtemp()
+        self.filepath = os.path.join(self.dirpath, 'somefile')
+        self.non_file = os.path.join(self.dirpath, 'nonfile')
+        open(self.filepath, 'wb').write('Hi!')
+        return
+
+    def tearDown(self):
+        if os.path.exists(self.dirpath):
+            shutil.rmtree(self.dirpath)
+        return
+
+    def test_handle_not_existing_path(self):
+        result = helpers.remove_file_or_directory(self.non_file)
+        self.assertTrue(result is None)
+        return
+
+    def test_handle_dir(self):
+        helpers.remove_file_or_directory(self.dirpath)
+        self.assertFalse(
+            os.path.exists(self.dirpath)
+            )
+        return
+
+    def test_handle_file(self):
+        helpers.remove_file_or_directory(self.filepath)
+        self.assertFalse(
+            os.path.exists(self.filepath)
+            )
+        return
+
+class CopyFileSystemTreeTestCase(unittest.TestCase):
+    # Test edge cases of copy_filesystem_tree().
+    #
+    # This is a typical case of tests not written as doctest as it is
+    # normally not interesting for developers and we only want to make
+    # sure everything works as expected.
+    def setUp(self):
+        self.existing_src = tempfile.mkdtemp()
+        self.filepath = os.path.join(self.existing_src, 'somefile')
+        open(self.filepath, 'wb').write('Hi!')
+        self.existing_dst = tempfile.mkdtemp()
+        self.not_existing_dir = tempfile.mkdtemp()
+        shutil.rmtree(self.not_existing_dir)
+
+        pass
+
+    def tearDown(self):
+        shutil.rmtree(self.existing_src)
+        shutil.rmtree(self.existing_dst)
+        pass
+
+    def test_source_and_dst_existing(self):
+        helpers.copy_filesystem_tree(self.existing_src, self.existing_dst)
+        self.assertTrue(
+            os.path.exists(
+                os.path.join(self.existing_dst, 'somefile')
+                )
+            )
+        return
+
+    def test_source_not_existing(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.not_existing_dir,
+            self.existing_dst
+            )
+        return
+
+    def test_dest_not_existing(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.existing_src,
+            self.not_existing_dir
+            )
+        return
+
+    def test_src_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.filepath,
+            self.existing_dst
+            )
+        return
+
+    def test_dst_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copy_filesystem_tree,
+            self.existing_src,
+            self.filepath
+            )
+        return
+
+class ReST2HTMLTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.expected = u'<div class="document">\n\n\n<p>Some '
+        self.expected += u'test with \xfcmlaut</p>\n</div>'
+        return
+
+    def test_ascii_umlauts(self):
+        # Make sure we convert umlauts correctly to unicode.
+        source = 'Some test with ümlaut'
+        result = helpers.ReST2HTML(source)
+        self.assertEqual(result, self.expected)
+
+    def test_unicode_umlauts(self):
+        # Make sure we convert umlauts correctly to unicode.
+        source = u'Some test with ümlaut'
+        result = helpers.ReST2HTML(source)
+        self.assertEqual(result, self.expected)
+
+    def test_unicode_output_from_ascii(self):
+        source = 'Some test with ümlaut'
+        self.assertTrue(isinstance(helpers.ReST2HTML(source), unicode))
+
+    def test_unicode_output_from_unicode(self):
+        source = u'Some test with ümlaut'
+        self.assertTrue(isinstance(helpers.ReST2HTML(source), unicode))
+
+
+class FactoryBaseTestCase(unittest.TestCase):
+
+    def test_ifaces(self):
+        # We test all relevant parts in the docstring. But the interfaces
+        # method has to be tested to please the coverage report as well.
+        factory = helpers.FactoryBase()
+        factory.factory = FakeObject
+        self.assertTrue(factory.getInterfaces()(IFakeObject))
+        return
+
+class CurrentPrincipalTestCase(unittest.TestCase):
+
+    def tearDown(test):
+        endInteraction() # Just in case, one is still lingering around
+
+    def test_existing_principal(self):
+        # We can get the current principal if one is involved
+        principal = Principal('myprincipal')
+        newInteraction(Participation(principal))
+        result = helpers.get_current_principal()
+        self.assertTrue(result is principal)
+
+    def test_no_participation(self):
+        # Interactions without participation are handled correctly
+        newInteraction()
+        result = helpers.get_current_principal()
+        self.assertTrue(result is None)
+
+    def test_not_existing_principal(self):
+        # Missing interactions do not raise errors.
+        result = helpers.get_current_principal()
+        self.assertTrue(result is None)
+
+class CmpFilesTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+    def test_equal(self):
+        p1 = os.path.join(self.workdir, 'sample1')
+        p2 = os.path.join(self.workdir, 'sample2')
+        open(p1, 'wb').write('Hi!')
+        open(p2, 'wb').write('Hi!')
+        assert helpers.cmp_files(open(p1, 'r'), open(p2, 'r')) is True
+
+    def test_unequal(self):
+        p1 = os.path.join(self.workdir, 'sample1')
+        p2 = os.path.join(self.workdir, 'sample2')
+        open(p1, 'wb').write('Hi!')
+        open(p2, 'wb').write('Ho!')
+        assert helpers.cmp_files(open(p1, 'r'), open(p2, 'r')) is False
+
+class FileSizeTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+    def test_real_file(self):
+        # we can get the size of real files
+        path = os.path.join(self.workdir, 'sample.txt')
+        open(path, 'wb').write('My content')
+        self.assertEqual(
+            int(helpers.file_size(open(path, 'rb'))), 10)
+        return
+
+    def test_stringio_file(self):
+        # we can get the size of file-like objects
+        self.assertEqual(
+            helpers.file_size(StringIO('my sample content')), 17)
+
+class IfaceNamesTestCase(unittest.TestCase):
+
+    def test_iface_names(self):
+        class I1(Interface):
+            foo = Attribute("""Some Foo""")
+            def bar(blah):
+                pass
+            i1_name = schema.TextLine(title=u'i1 name')
+        class I2(I1):
+            baz = schema.TextLine(title=u'some baz')
+        class I3(I2):
+            pass
+
+        result1 = helpers.iface_names(I3)
+        result2 = helpers.iface_names(I2)
+        result3 = helpers.iface_names(I1)
+        result4 = helpers.iface_names(I3, exclude_attribs=False)
+        result5 = helpers.iface_names(I3, exclude_methods=False)
+        result6 = helpers.iface_names(I3, omit='i1_name')
+        self.assertEqual(sorted(result1), ['baz', 'i1_name'])
+        self.assertEqual(sorted(result2), ['baz', 'i1_name'])
+        self.assertEqual(sorted(result3), ['i1_name'])
+        self.assertEqual(sorted(result4), ['baz', 'foo', 'i1_name'])
+        self.assertEqual(sorted(result5), ['bar', 'baz', 'i1_name'])
+        self.assertEqual(sorted(result6), ['baz'])
+        return
+
+class DateTimeHelpersTestCase(unittest.TestCase):
+
+    def test_now(self):
+        tz_berlin = pytz.timezone('Europe/Berlin')
+        result1 = helpers.now()
+        result2 = helpers.now(tz_berlin)
+        self.assertEqual(result1.tzinfo, pytz.utc)
+        self.assertFalse(result2.tzinfo == pytz.utc)
+        self.assertFalse(result2.tzinfo is None)
+        return
+
+    def test_to_timezone(self):
+        fmt = '%Y-%m-%d %H:%M:%S %Z%z'
+        tz_berlin = pytz.timezone('Europe/Berlin')
+        tz_lagos = pytz.timezone('Africa/Lagos')
+        dt1 = datetime.datetime(2012, 1, 1, 0, 0)
+        dt2 = datetime.datetime(2012, 1, 1, 0, 0, tzinfo=tz_berlin)
+        dt3 = datetime.datetime(2012, 6, 1, 0, 0, tzinfo=tz_lagos)
+        dt4 = datetime.datetime(2012, 6, 1, 0, 0, tzinfo=tz_berlin)
+        result1 = helpers.to_timezone(dt1)
+        result2 = helpers.to_timezone(dt1, pytz.utc)
+        result3 = helpers.to_timezone(dt2)
+        result4 = helpers.to_timezone(dt2, tz_lagos)
+        result5 = helpers.to_timezone(dt3, tz_berlin)
+        result6 = helpers.to_timezone(dt4, tz_lagos)
+        self.assertEqual(
+            result1.strftime(fmt), '2012-01-01 00:00:00 UTC+0000')
+        self.assertEqual(
+            result2.strftime(fmt), '2012-01-01 00:00:00 UTC+0000')
+        self.assertEqual(
+            result3.strftime(fmt), '2011-12-31 23:00:00 UTC+0000')
+        self.assertEqual(
+            result4.strftime(fmt), '2012-01-01 00:00:00 WAT+0100')
+        self.assertEqual(
+            result5.strftime(fmt), '2012-06-01 01:46:00 CEST+0200')
+        self.assertEqual(
+            result6.strftime(fmt), '2012-06-01 00:00:00 WAT+0100')
+        return
+
+    def test_to_timezone_no_dt(self):
+        # the to_timezone function copes with dates (!= datetimes)
+        d = datetime.date(2012, 12, 1)
+        result1 = helpers.to_timezone(d)
+        result2 = helpers.to_timezone(d, pytz.utc)
+        self.assertEqual(result1, d)
+        self.assertEqual(result2, d)
+        return
+
+class GetFileFormatTestCase(unittest.TestCase):
+    # Tests for the get_fileformat helper.
+
+    def setUp(self):
+        self.valid_jpg_path = os.path.join(
+            os.path.dirname(__file__), 'sample_jpg_valid.jpg')
+        self.valid_jpg = open(self.valid_jpg_path, 'rb').read()
+        self.valid_png_path = os.path.join(
+            os.path.dirname(__file__), 'sample_png_valid.png')
+        self.valid_png = open(self.valid_png_path, 'rb').read()
+        self.valid_pdf_path = os.path.join(
+            os.path.dirname(__file__), 'sample_pdf_valid.pdf')
+        self.valid_pdf = open(self.valid_pdf_path, 'rb').read()
+        self.valid_fpm_path = os.path.join(
+            os.path.dirname(__file__), 'sample_fpm_valid.fpm')
+        self.valid_fpm = open(self.valid_fpm_path, 'rb').read()
+        return
+
+    def test_none(self):
+        # ``None`` is not a file and not a valid file format
+        self.assertEqual(helpers.get_fileformat(None), None)
+        return
+
+    def test_path_and_bytestream(self):
+        # get_fileformat accepts bytestreams and paths as arg.
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_jpg), 'jpg')
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_jpg_path), 'jpg')
+        # path is ignored when giving a bytestream
+        self.assertEqual(
+            helpers.get_fileformat('blah', self.valid_jpg), 'jpg')
+        return
+
+    def test_jpg(self):
+        # we recognize jpeg images.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_jpg_path), 'jpg')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_jpg), 'jpg')
+        return
+
+    def test_png(self):
+        # we recognize png images.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_png_path), 'png')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_png), 'png')
+        return
+
+    def test_pdf(self):
+        # we recognize pdf documents.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_pdf_path), 'pdf')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_pdf), 'pdf')
+        return
+
+    def test_fpm(self):
+        # we recognize fpm files.
+        # fpm files are binary fingerprint data produced by libfprint.
+        self.assertEqual(
+            helpers.get_fileformat(self.valid_fpm_path), 'fpm')
+        self.assertEqual(
+            helpers.get_fileformat(None, self.valid_fpm), 'fpm')
+        return
+
+class MergeCSVFileTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.path1 = os.path.join(self.workdir, 'myfile1')
+        self.path2 = os.path.join(self.workdir, 'myfile2')
+        self.result_path = None
+        return
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+        if self.result_path is not None and os.path.exists(self.result_path):
+            os.unlink(self.result_path)
+        return
+
+    def test_basic(self):
+        # we can merge very basic CSV files
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('name,age\nBarney,28\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n'
+            '28,Barney\r\n')
+        return
+
+    def test_different_col_order(self):
+        # if cols of both files have different order, that won't stop us
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('age,name\n28,Barney\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n'
+            '28,Barney\r\n')
+        return
+
+    def test_different_cols_at_all(self):
+        # also cols available only in one file will work.
+        open(self.path1, 'wb').write('name,age\nManfred,32\n')
+        open(self.path2, 'wb').write('name,age,buddy\nBarney,28,Manfred\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,buddy,name\r\n'
+            '32,,Manfred\r\n'
+            '28,Manfred,Barney\r\n')
+        return
+
+    def test_one_empty_input(self):
+        # we cope even with nearly empty input (one file with no data)
+        open(self.path1, 'wb').write('\n')
+        open(self.path2, 'wb').write('name,age\nManfred,32\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents,
+            'age,name\r\n'
+            '32,Manfred\r\n')
+        return
+
+    def test_two_empty_inputs(self):
+        # we cope even with empty input (two files with no data)
+        open(self.path1, 'wb').write('\n')
+        open(self.path2, 'wb').write('\n')
+        self.result_path = helpers.merge_csv_files(self.path1, self.path2)
+        contents = open(self.result_path, 'r').read()
+        self.assertEqual(
+            contents, '\r\n')
+        return
+
+
+class CheckCSVCharsetTestCase(unittest.TestCase):
+
+    def test_valid_data1(self):
+        csv = (
+            'col1,col2,col3\n'
+            'val1,val2,val3\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), None)
+
+    def test_valid_data2(self):
+        csv = (
+            "code,title,title_prefix\n"
+            "FAC1,Faculty 1,faculty\n"
+            "FAC2,Faculty 2,institute\n"
+            "FAC3,Fäcülty 3,school\n"
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), None)
+
+    def test_invalid_data1(self):
+        csv = (
+            'col1,col2,col3\n' +
+            chr(0x92) + 'val1,val2,val3\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), 1)
+
+    def test_invalid_data2(self):
+        csv = (
+            'some text that \n'
+            '\n'      # this empty line will break
+            'is not a csv file\n' + chr(0x92) + '\n'
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), 2)
+
+
+    def test_invalid_data3(self):
+        csv = (
+            "code,title,title_prefix\n"
+            "FAC1,Faculty 1,faculty\n"
+            "FAC2,Faculty 2 ,institute\n"
+            "FAC3,Fäcülty 3,school\n"
+            ).splitlines()
+        self.assertEqual(helpers.check_csv_charset(csv), -1)
+
+class MemInfoTestCase(unittest.TestCase):
+
+    def test_getsetattrs(self):
+        # we can set/get attributes of MemInfos
+        info = helpers.MemInfo()
+        self.assertRaises(
+            KeyError, info.__getattr__, 'foo')
+        info['foo'] = 'bar'
+        assert info.foo == 'bar'
+        info.bar = 'baz'
+        assert info.bar == 'baz'
+
+    def test_getattrs(self):
+        # we can del attributes
+        info = helpers.MemInfo()
+        info['foo'] = 'bar'
+        del info['foo']
+        self.assertRaises(
+            KeyError, info.__getattr__, 'foo')
+        info['bar'] = 'baz'
+        del info.bar
+        self.assertRaises(
+            KeyError, info.__getattr__, 'bar')
+
+
+class GetMemInfoTestCase(unittest.TestCase):
+
+    @unittest.skipIf(
+        not os.path.exists('/proc/meminfo'),
+        reason="No /proc/meminfo found.")
+    def test_system(self):
+        info = helpers.get_meminfo()
+        assert isinstance(info, helpers.MemInfo)
+
+    def test_values(self):
+        sample_meminfo = os.path.join(
+            os.path.dirname(__file__), 'sample_meminfo')
+        info = helpers.get_meminfo(src=sample_meminfo)
+        assert info.Cached == 1013816
+
+    def test_invalid_src(self):
+        # we cope with invalid src files
+        info = helpers.get_meminfo(src="nOt-ExIsTiNg-FiLe")
+        assert info is None
+
+
+class Html2dictTestCase(unittest.TestCase):
+
+    def test_html2dict(self):
+        assert helpers.html2dict(None) == {}
+        assert helpers.html2dict() == {}
+        assert helpers.html2dict(9) == {}
+        assert helpers.html2dict('Hello world') == {
+            'en': u'<div id="html">Hello world</div id="html">'}
+        assert helpers.html2dict('Hello world>>de<<Hallo Welt') == {
+            'de': u'<div id="html">Hallo Welt</div id="html">',
+            'en': u'<div id="html">Hello world</div id="html">'}
+
+
+class Rest2dictTestCase(unittest.TestCase):
+
+    def test_rest2dict(self):
+        assert helpers.rest2dict(None) == {}
+        assert helpers.rest2dict() == {}
+        assert helpers.rest2dict(9) == {}
+        assert helpers.rest2dict('Hello world') == {
+            'en': u'<div id="rest"><div class="document">\n\n\n<p>Hello world'
+                   '</p>\n</div></div id="rest">'}
+        assert helpers.rest2dict('Hello world>>de<<Hallo Welt') == {
+            'de': u'<div id="rest"><div class="document">\n\n\n<p>Hallo Welt'
+                   '</p>\n</div></div id="rest">',
+            'en': u'<div id="rest"><div class="document">\n\n\n<p>Hello world'
+                   '</p>\n</div></div id="rest">'}
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    # Register local test cases...
+    for testcase in [
+        ReST2HTMLTestCase,
+        FactoryBaseTestCase,
+        CopyFileSystemTreeTestCase,
+        RemoveFileOrDirectoryTestCase,
+        CurrentPrincipalTestCase,
+        CmpFilesTestCase,
+        FileSizeTestCase,
+        IfaceNamesTestCase,
+        DateTimeHelpersTestCase,
+        GetFileFormatTestCase,
+        MergeCSVFileTestCase,
+        SimpleHelpersTestCase,
+        CheckCSVCharsetTestCase,
+        MemInfoTestCase,
+        GetMemInfoTestCase,
+        Html2dictTestCase,
+        Rest2dictTestCase,
+        ]:
+        suite.addTests(
+            unittest.TestLoader().loadTestsFromTestCase(testcase)
+            )
+    # Add tests from docstrings in helpers.py...
+    suite.addTests(
+        doctest.DocTestSuite(
+            helpers,
+            optionflags = doctest.ELLIPSIS + doctest.REPORT_NDIFF,
+            )
+        )
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_logger.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_logger.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_logger.py	(revision 16045)
@@ -0,0 +1,422 @@
+## $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
+##
+
+# Tests for waeup.kofa.utils.logger
+import logging
+import os
+import shutil
+import tempfile
+from zope.component import queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.kofa.app import University
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+from waeup.kofa.utils.logger import (
+    Logger, ILoggerCollector, LoggerCollector,
+    ILogger)
+
+class FakeComponent(object):
+    # A component that can use the Logger mixin-class
+    def do_something(self):
+        self.logger.info('msg from do_something')
+
+class LoggingComponent(FakeComponent, Logger):
+    # A component that supports logging by mixing in Logger
+    logger_name = 'sample.${sitename}'
+    logger_filename = 'sample.log'
+
+class LoggersTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggersTests, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root2 = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(LoggersTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def clear_logger(self, logger):
+        del_list = [x for x in logger.handlers]
+        for handler in del_list:
+            logger.removeHandler(handler)
+        return
+
+    def test_iface(self):
+        setSite(self.app)
+        logger = LoggingComponent()
+        verifyClass(ILogger, Logger)
+        verifyObject(ILogger, logger)
+        return
+
+    def test_logger_prop_no_site(self):
+        # If not in a site, we will get a logger without handlers
+        logger = Logger().logger
+        self.assertTrue(isinstance(logger, logging.Logger))
+        self.assertEqual(len(logger.handlers), 0)
+        return
+
+    def test_site_no_site_name(self):
+        # If a site has not name we will get a logger anyway
+        setSite(self.app)
+        del self.app.__name__
+        logger = Logger().logger
+        self.assertTrue(logger is not None)
+        return
+
+    def test_get_logdir(self):
+        # We can get a logdir
+        setSite(self.app)
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        self.assertEqual(logdir, expected_logdir)
+        return
+
+    def test_get_logdir_no_site(self):
+        # We cannot get a logdir without a site
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_no_datacenter(self):
+        # Having a site, is not enough. We also need a datacenter in it.
+        setSite(self.app)
+        del self.app['datacenter']
+        logger = Logger()
+        logdir = logger.logger_get_logdir()
+        self.assertTrue(logdir is None)
+        return
+
+    def test_get_logdir_create_dir(self):
+        # If the logdirectory does not exist, we will create it
+        setSite(self.app)
+        logger = Logger()
+        expected_logdir = os.path.join(
+            self.app['datacenter'].storage, 'logs')
+        if os.path.exists(expected_logdir):
+            shutil.rmtree(expected_logdir)
+        logdir = logger.logger_get_logdir()
+        exists_after = os.path.exists(expected_logdir)
+        self.assertEqual(logdir, expected_logdir)
+        self.assertTrue(exists_after is True)
+        return
+
+    def test_get_logfile(self):
+        # We can get a logfile
+        setSite(self.app)
+        logger = Logger()
+        expected_filepath = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        path = logger.logger_get_logfile_path()
+        self.assertEqual(expected_filepath, path)
+        return
+
+    def test_get_logfile_no_dir(self):
+        # We cannot get a logfilepath if there is no dir for it
+        setSite(self.app)
+        logger = Logger()
+        del self.app['datacenter']
+        path = logger.logger_get_logfile_path()
+        self.assertTrue(path is None)
+        return
+
+    def test_setup(self):
+        # We can setup a logger.
+        mylogger = logging.getLogger('test.sample')
+        setSite(self.app)
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertEqual(len(result.handlers), 1)
+        handler = result.handlers[0]
+        self.assertTrue(result.propagate is False)
+        return
+
+    def test_setup_no_site(self):
+        # Without a site we get no logger from setup
+        mylogger = logging.getLogger('test.sample')
+        logger = Logger()
+        result = logger.logger_setup(mylogger)
+        self.assertTrue(result is None)
+        return
+
+    def test_logfile_change(self):
+        # When the logfile location changes, logger_logfile_changed can react
+        setSite(self.app)
+        logger = Logger()
+        logger.logger.warn('Warning 1')  # Log something to old logfile
+        filename1 = logger.logger.handlers[0].baseFilename
+        content1 = open(filename1, 'r').read()
+        # Now move the logfile
+        self.dc_root2 = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root2)
+        logger.logger_logfile_changed()
+        logger.logger.warn('Warning 2')  # Log something to new logfile
+        filename2 = logger.logger.handlers[0].baseFilename
+        content2 = open(filename2, 'r').read()
+        self.assertTrue('Warning 1' in content1)
+        self.assertTrue('Warning 2' in content2)
+        self.assertTrue('Warning 1' not in content2)
+        self.assertTrue('Warning 2' not in content1)
+        self.assertTrue(filename1 != filename2)
+        shutil.rmtree(self.dc_root2)
+        return
+
+    def test_logger_additional_handlers(self):
+        # When we detect additional handlers in a logger, setup anew.
+        setSite(self.app)
+        logger = Logger()
+        old_py_logger = logger.logger
+        handler = logging.StreamHandler()
+        logger.logger.addHandler(handler)
+        old_handler_num = len(old_py_logger.handlers)
+        new_py_logger = logger.logger  # This should detect new handler
+        new_handler_num = len(new_py_logger.handlers)
+        self.assertEqual(new_handler_num, 1)
+        self.assertTrue(isinstance(
+            new_py_logger.handlers[0],
+            logging.handlers.WatchedFileHandler))
+
+class LoggersFunctionalTests(FunctionalTestCase):
+    # Check loggers with real components using them
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggersFunctionalTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+        return
+
+    def tearDown(self):
+        super(LoggersFunctionalTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_component_can_get_logger(self):
+        # Components can get a logger
+        logger = self.component.logger
+        self.assertTrue(logger is not None)
+        self.assertEqual(len(logger.handlers), 1)
+        self.assertEqual(logger.name, 'sample.app')
+        return
+
+    def test_component_handler_setup(self):
+        # handlers of components are setup
+        handler = self.component.logger.handlers[0]
+        self.assertTrue(isinstance(
+                handler, logging.handlers.WatchedFileHandler))
+        if hasattr(handler, 'maxBytes'):
+            self.assertEqual(handler.maxBytes, MAX_BYTES)
+        if hasattr(handler, 'backupCount'):
+            self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        self.assertTrue(handler.baseFilename.endswith('logs/sample.log'))
+        return
+
+    def test_component_can_log(self):
+        # a method of some 'loggerized' instance can log like this.
+        self.component.logger.setLevel(logging.INFO)
+        self.component.do_something()
+        handler = logging.getLogger('sample.app').handlers[0]
+        logfile = handler.baseFilename
+        content = open(logfile, 'rb').read()
+        self.assertTrue(content.endswith(' - INFO - system - msg from do_something\n'))
+        return
+
+class LoggerCollectorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggerCollectorTests, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+
+        self.component = LoggingComponent()
+
+
+        return
+
+    def tearDown(self):
+        super(LoggerCollectorTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_ifaces(self):
+        collector = LoggerCollector()
+        verifyClass(ILoggerCollector, LoggerCollector)
+        verifyObject(ILoggerCollector, collector)
+        return
+
+    def test_get_utility(self):
+        # We can get a logger collector via utility lookup
+        util1 = queryUtility(ILoggerCollector, default=None)
+        util2 = queryUtility(ILoggerCollector, default=None)
+        self.assertTrue(util1 is util2)
+        self.assertTrue(isinstance(util1, LoggerCollector))
+        return
+
+    def test_get_loggers(self):
+        # We can get loggers from a logger collector
+        collector = LoggerCollector()
+        result0 = collector.getLoggers(None)
+        result1 = collector.getLoggers('not-a-site')
+        result2 = collector.getLoggers(self.app)
+        collector.registerLogger(self.app, self.component)
+        result3 = collector.getLoggers(self.app)
+        self.assertEqual(result0, [])
+        self.assertEqual(result1, [])
+        self.assertEqual(result2, [])
+        self.assertEqual(result3, [self.component])
+        self.assertTrue(result3[0] is self.component)
+        return
+
+    def test_register_logger(self):
+        # We can register loggers by name
+        collector = LoggerCollector()
+        collector.registerLogger(None, self.component) # Should have no effect
+        collector.registerLogger(object(), self.component) # Same here
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(['app'], collector.keys())
+        return
+
+    def test_register_logger_double(self):
+        # Registering the same component twice gives one entry, not two
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.registerLogger(self.app, self.component)
+        self.assertEqual(len(collector['app']), 1)
+        return
+
+    def test_unregister_logger(self):
+        # We can also unregister loggers
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(self.app, self.component)
+        result = collector.getLoggers(self.app)
+        self.assertEqual(result, [])
+        return
+
+    def test_unregister_logger_invalid(self):
+        # We can cope with unregistration of non-existing entries.
+        collector = LoggerCollector()
+        collector.registerLogger(self.app, self.component)
+        collector.unregisterLogger(None, self.component)
+        collector.unregisterLogger(self.app, 'nonsense')
+        self.assertEqual(len(collector), 1)
+        return
+
+class LogfileChangeTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LogfileChangeTests, self).setUp()
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        self.dc_root_new = None
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+        self.workdir = tempfile.mkdtemp()
+
+        self.app['mycomponent'] = LoggingComponent()
+        self.component = self.app['mycomponent']
+        setSite(self.app)
+        self.component = LoggingComponent()
+        return
+
+    def tearDown(self):
+        super(LogfileChangeTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        if self.dc_root_new is not None:
+            shutil.rmtree(self.dc_root_new)
+        clearSite()
+        return
+
+    def test_storage_move(self):
+        # When a datacenter storage (its path) changes, we have to
+        # move the logfiles as well.
+        filename1 = self.component.logger.handlers[0].baseFilename
+        self.dc_root_new = tempfile.mkdtemp()
+        self.app['datacenter'].setStoragePath(self.dc_root_new)
+        filename2 = self.component.logger.handlers[0].baseFilename
+        self.assertTrue(filename1 != filename2)
+        self.assertTrue(filename2.startswith(self.dc_root_new))
+        return
+
+    def test_storage_deletion(self):
+        # When a site is deleted, the loggers should go away as well
+        self.dc_root_new = tempfile.mkdtemp()
+        logger1 = self.component.logger.handlers # create entry in collector
+        collector = queryUtility(ILoggerCollector)
+        loggers1 = collector.keys()
+        root = self.app.__parent__
+        del root['app']
+        loggers2 = collector.keys()
+        self.assertEqual(loggers1, ['app'])
+        self.assertEqual(loggers2, [])
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/tests/test_utils.py	(revision 16045)
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+## $Id$
+##
+## Copyright (C) 2014 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
+##
+import psutil
+import sys
+import unittest
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.utils import KofaUtils
+from zope.interface import verify
+
+
+class KofaUtilsTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.max_vmem = psutil.virtual_memory().total
+        self.max_smem = psutil.swap_memory().total
+
+    def get_cleared_util(self):
+        # Helper: get a `KofUtil` instance with all values of
+        # SYSTEM_MAX_LOAD dict set to ``None``
+        util = KofaUtils()
+        for key, val in util.SYSTEM_MAX_LOAD.items():
+            util.SYSTEM_MAX_LOAD[key] = None
+        return util
+
+    def test_iface(self):
+        # KofaUtils fullfill IKofaUtils expectations
+        utils = KofaUtils()
+        verify.verifyClass(IKofaUtils, KofaUtils)
+        verify.verifyObject(IKofaUtils, utils)
+
+    def test_expensive_actions_allowed_swap_none(self):
+        # unset swap maximum values make KofUtils ignore swap values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = None
+        assert utils.expensive_actions_allowed() == True
+        # even not-set values won't block us
+        del utils.SYSTEM_MAX_LOAD['swap-mem']
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        psutil.swap_memory().percent >= 99.0,
+        reason="System swap use over 99%. Cannot set higher allowed value.")
+    def test_expensive_actions_allowed_swap_ok(self):
+        # We can react to high swap values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 99.0           # positive float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -1.0           # negative float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = sys.maxint     # positive int
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -1             # negative int
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        not psutil.swap_memory().percent,
+        reason="Can test swapping behavior only if actually swapping")
+    def test_expensive_actions_allowed_swap_too_much(self):
+        # We can react if too much swap is used
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 0.0            # positive float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -100.0         # negative float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = 0              # positive int
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['swap-mem'] = -(sys.maxint)  # negative int
+        assert utils.expensive_actions_allowed() == False
+
+    def test_expensive_actions_allowed_virtmem_none(self):
+        # unset virtmem maximum values make KofUtils ignore virtmem values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = None
+        assert utils.expensive_actions_allowed() == True
+        # even not-set values won't block us
+        del utils.SYSTEM_MAX_LOAD['virt-mem']
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        psutil.virtual_memory().percent >= 99.0,
+        reason="System virtmem use over 99%. Cannot set higher allowed value.")
+    def test_expensive_actions_allowed_virtmem_ok(self):
+        # We can react to high virtmem values
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 99.0           # positive float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -1.0           # negative float
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = sys.maxint     # positive int
+        assert utils.expensive_actions_allowed() == True
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -1             # negative int
+        assert utils.expensive_actions_allowed() == True
+
+    @unittest.skipIf(
+        not psutil.virtual_memory().percent,
+        reason="Can test virtmem behavior only if actually using some")
+    def test_expensive_actions_allowed_virtmem_too_much(self):
+        # We can react if too much virtmem is used
+        utils = self.get_cleared_util()
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 0.0            # positive float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -100.0         # negative float
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = 0              # positive int
+        assert utils.expensive_actions_allowed() == False
+        utils.SYSTEM_MAX_LOAD['virt-mem'] = -(sys.maxint)  # negative int
+        assert utils.expensive_actions_allowed() == False
+
+    def test_format_float(self):
+        format_float= KofaUtils().format_float
+        # This is the floating point problem ...
+        self.assertEqual(4.6*100, 459.99999999999994)
+        # ... which is solved in format_float
+        self.assertEqual(format_float(4.6 ,1), '4.6')
+        self.assertEqual(format_float(4.6 ,2), '4.60')
+        self.assertEqual(format_float(4.6 ,3), '4.600')
+        self.assertEqual(format_float(4.6 ,4), '4.6000')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/utils.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/utils/utils.py	(revision 16045)
@@ -0,0 +1,446 @@
+## $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
+##
+"""General helper utilities for Kofa.
+"""
+import grok
+import psutil
+import string
+import pytz
+import decimal
+from copy import deepcopy
+from random import SystemRandom as r
+from zope.i18n import translate
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.smtp import send_mail as send_mail_internally
+from waeup.kofa.utils.helpers import get_sorted_preferred
+from waeup.kofa.utils.degrees import DEGREES_DICT
+
+
+def send_mail(from_name, from_addr,
+              rcpt_name, rcpt_addr,
+              subject, body, config):
+    """Wrapper for the real SMTP functionality in :mod:`waeup.kofa.smtp`.
+
+    Merely here to stay compatible with lots of calls to this place.
+    """
+    mail_id = send_mail_internally(
+        from_name, from_addr, rcpt_name, rcpt_addr,
+        subject, body, config)
+    return True
+
+
+#: A list of phone prefixes (order num, country, prefix).
+#: Items with same order num will be sorted alphabetically.
+#: The lower the order num, the higher the precedence.
+INT_PHONE_PREFIXES = [
+    (99, _('Germany'), '49'),
+    (1, _('Nigeria'), '234'),
+    (99, _('U.S.'), '1'),
+    ]
+
+
+def sorted_phone_prefixes(data=INT_PHONE_PREFIXES, request=None):
+    """Sorted tuples of phone prefixes.
+
+    Ordered as shown above and formatted for use in select boxes.
+
+    If request is given, we'll try to translate all country names in
+    order to sort alphabetically correctly.
+
+    XXX: This is a function (and not a constant) as different
+    languages might give different orders. This is not tested yet.
+
+    XXX: If we really want to use alphabetic ordering here, we might
+    think about caching results of translations.
+    """
+    if request is not None:
+        data = [
+            (x, translate(y, context=request), z)
+            for x, y, z in data]
+    return tuple([
+        ('%s (+%s)' % (x[1], x[2]), '+%s' % x[2])
+        for x in sorted(data)
+        ])
+
+
+class KofaUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(IKofaUtils)
+
+    #: This the only place where we define the portal language
+    #: which is used for the translation of system messages
+    #: (e.g. object histories) pdf slips.
+    PORTAL_LANGUAGE = 'en'
+
+    DEGREES_DICT = DEGREES_DICT
+
+    PREFERRED_LANGUAGES_DICT = {
+        'en': (1, u'English'),
+        'fr': (2, u'Fran&ccedil;ais'),
+        'de': (3, u'Deutsch'),
+        'ha': (4, u'Hausa'),
+        'yo': (5, u'Yoruba'),
+        'ig': (6, u'Igbo'),
+        }
+
+    #: A function to return
+    @classmethod
+    def sorted_phone_prefixes(cls, data=INT_PHONE_PREFIXES, request=None):
+        return sorted_phone_prefixes(data, request)
+
+    EXAM_SUBJECTS_DICT = {
+        'math': 'Mathematics',
+        'computer_science': 'Computer Science',
+        }
+
+    #: Exam grades. The tuple is sorted as it should be displayed in
+    #: select boxes.
+    EXAM_GRADES = (
+        ('A', 'Best'),
+        ('B', 'Better'),
+        ('C', 'Good'),
+        )
+
+    INST_TYPES_DICT = {
+        'none': '',
+        'faculty': 'Faculty of',
+        'department': 'Department of',
+        'school': 'School of',
+        'office': 'Office for',
+        'centre': 'Centre for',
+        'institute': 'Institute of',
+        'school_for': 'School for',
+        'college': 'College of',
+        'directorate': 'Directorate of',
+        }
+
+    STUDY_MODES_DICT = {
+        'transfer': 'Transfer',
+        'ug_ft': 'Undergraduate Full-Time',
+        'ug_pt': 'Undergraduate Part-Time',
+        'pg_ft': 'Postgraduate Full-Time',
+        'pg_pt': 'Postgraduate Part-Time',
+        }
+
+    DISABLE_PAYMENT_GROUP_DICT = {
+        'sf_all': 'School Fee - All Students',
+        }
+
+    APP_CATS_DICT = {
+        'basic': 'Basic Application',
+        'no': 'no application',
+        'pg': 'Postgraduate',
+        'sandwich': 'Sandwich',
+        'cest': 'Part-Time, Diploma, Certificate'
+        }
+
+    SEMESTER_DICT = {
+        1: '1st Semester',
+        2: '2nd Semester',
+        3: 'Combined',
+        9: 'N/A'
+        }
+
+    COURSE_CATEGORY_DICT = {
+        }
+
+    SPECIAL_HANDLING_DICT = {
+        'regular': 'Regular Hostel',
+        'blocked': 'Blocked Hostel',
+        'pg': 'Postgraduate Hostel'
+        }
+
+    SPECIAL_APP_DICT = {
+        'transcript': 'Transcript Fee Payment',
+        'clearance': 'Acceptance Fee',
+        }
+
+    PAYMENT_CATEGORIES = {
+        'schoolfee': 'School Fee',
+        'clearance': 'Acceptance Fee',
+        'bed_allocation': 'Bed Allocation Fee',
+        'hostel_maintenance': 'Hostel Maintenance Fee',
+        'transfer': 'Transfer Fee',
+        'gown': 'Gown Hire Fee',
+        'application': 'Application Fee',
+        'app_balance': 'Application Fee Balance',
+        'transcript': 'Transcript Fee',
+        'late_registration': 'Late Course Registration Fee',
+        'combi': 'Combi Payment',
+        }
+
+    def selectable_payment_categories(self, student):
+        return self.PAYMENT_CATEGORIES
+
+    PREVIOUS_PAYMENT_CATEGORIES = deepcopy(PAYMENT_CATEGORIES)
+
+    REPORTABLE_PAYMENT_CATEGORIES = {
+        'schoolfee': 'School Fee',
+        'clearance': 'Acceptance Fee',
+        'hostel_maintenance': 'Hostel Maintenance Fee',
+        'gown': 'Gown Hire Fee',
+        }
+
+    BALANCE_PAYMENT_CATEGORIES = {
+        'schoolfee': 'School Fee',
+        }
+
+    COMBI_PAYMENT_CATEGORIES = {
+        'gown': 'Gown Hire Fee',
+        'transcript': 'Transcript Fee',
+        'late_registration': 'Late Course Registration Fee',
+        }
+
+    MODE_GROUPS = {
+        'All': ('all',),
+        'Undergraduate Full-Time': ('ug_ft',),
+        'Undergraduate Part-Time': ('ug_pt',),
+        'Postgraduate Full-Time': ('pg_ft',),
+        'Postgraduate Part-Time': ('pg_pt',),
+        }
+
+    VERDICTS_DICT = {
+        '0': _('(not yet)'),
+        'A': 'Successful student',
+        'B': 'Student with carryover courses',
+        'C': 'Student on probation',
+        }
+
+    #: Set positive number for allowed max, negative for required min
+    #: avail.
+    #: Use integer for bytes value, float for percent
+    #: value. `cpu-load`, of course, accepts float values only.
+    #: `swap-mem` = Swap Memory, `virt-mem` = Virtual Memory,
+    #: `cpu-load` = CPU load in percent.
+    SYSTEM_MAX_LOAD = {
+        'swap-mem': None,
+        'virt-mem': None,
+        'cpu-load': 100.0,
+        }
+
+    #: Maximum number of files listed in `finished` subfolder
+    MAX_FILES = 100
+
+    #: Maximum size in Bytes of passport images in the applicants and
+    #: students section
+    MAX_PASSPORT_SIZE = 50 * 1024
+
+    #: Temporary passwords and parents password validity period
+    TEMP_PASSWORD_MINUTES = 10
+
+    def sendContactForm(self, from_name, from_addr, rcpt_name, rcpt_addr,
+                        from_username, usertype, portal, body, subject):
+        """Send an email with data provided by forms.
+        """
+        config = grok.getSite()['configuration']
+        text = _(u"""Fullname: ${a}
+User Id: ${b}
+User Type: ${c}
+Portal: ${d}
+
+${e}
+""")
+        text = _(text, mapping={
+            'a': from_name,
+            'b': from_username,
+            'c': usertype,
+            'd': portal,
+            'e': body})
+        body = translate(text, 'waeup.kofa',
+            target_language=self.PORTAL_LANGUAGE)
+        if not (from_addr and rcpt_addr):
+            return False
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    def getUsers(self):
+        users = sorted(
+            grok.getSite()['users'].items(), key=lambda x: x[1].title)
+        for key, val in users:
+            yield(dict(name=key, val="%s (%s)" % (val.title, val.name)))
+
+    @property
+    def tzinfo(self):
+        """Time zone of the university.
+        """
+        # For Nigeria: pytz.timezone('Africa/Lagos')
+        # For Germany: pytz.timezone('Europe/Berlin')
+        return pytz.utc
+
+    def fullname(self, firstname, lastname, middlename=None):
+        """Construct fullname.
+        """
+        # We do not necessarily have the middlename attribute
+        if middlename:
+            name = '%s %s %s' % (firstname, middlename, lastname)
+        else:
+            name = '%s %s' % (firstname, lastname)
+        if '<' in name:
+            return 'XXX'
+        return string.capwords(
+            name.replace('-', ' - ')).replace(' - ', '-')
+
+    def genPassword(self, length=4, chars=string.letters + string.digits):
+        """Generate a random password.
+        """
+        return ''.join([
+            r().choice(string.uppercase) +
+            r().choice(string.lowercase) +
+            r().choice(string.digits) for i in range(length)])
+
+    def sendCredentials(self, user, password=None, url_info=None, msg=None):
+        """Send credentials as email. Input is the user for which credentials
+        are sent and the password. Method returns True or False to indicate
+        successful operation.
+        """
+        subject = 'Your Kofa credentials'
+        text = _(u"""Dear ${a},
+
+${b}
+Student Registration and Information Portal of
+${c}.
+
+Your user name: ${d}
+Your password: ${e}
+${f}
+
+Please remember your user name and keep
+your password secret!
+
+Please also note that passwords are case-sensitive.
+
+Regards
+""")
+        config = grok.getSite()['configuration']
+        from_name = config.name_admin
+        from_addr = config.email_admin
+        rcpt_name = user.title
+        rcpt_addr = user.email
+        text = _(text, mapping={
+            'a': rcpt_name,
+            'b': msg,
+            'c': config.name,
+            'd': user.name,
+            'e': password,
+            'f': url_info})
+
+        body = translate(text, 'waeup.kofa',
+            target_language=self.PORTAL_LANGUAGE)
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    def inviteReferee(self, referee, applicant, url_info=None):
+        """Send invitation email to referee.
+        """
+        config = grok.getSite()['configuration']
+        subject = 'Request for referee report from %s' % config.name
+        text = _(u"""Dear ${a},
+
+The candidate with Id ${b} and name ${c} applied to
+the ${d} to study ${e} for the ${f} session.
+The candidate has listed you as referee. You are thus required to kindly use
+the link below to provide your referral remarks on or before
+${g}.
+
+${h}
+
+Thank You
+
+The Secretary
+Post Graduate School
+${d}
+""")
+        from_name = config.name_admin
+        from_addr = config.email_admin
+        rcpt_name = referee.name
+        rcpt_addr = referee.email
+        session = '%s/%s' % (
+            applicant.__parent__.year, applicant.__parent__.year+1)
+        text = _(text, mapping={
+            'a': rcpt_name,
+            'b': applicant.applicant_id,
+            'c': applicant.display_fullname,
+            'd': config.name,
+            'e': applicant.course1.title,
+            'f': session,
+            'g': applicant.__parent__.enddate,
+            'h': url_info,
+            })
+
+        body = translate(text, 'waeup.kofa',
+            target_language=self.PORTAL_LANGUAGE)
+        return send_mail(
+            from_name, from_addr, rcpt_name, rcpt_addr,
+            subject, body, config)
+
+    def getPaymentItem(self, payment):
+        """Return payment item. This method can be used to customize the
+        `display_item` property attribute, e.g. in order to hide bed coordinates
+        if maintenance fee is not paid.
+        """
+        return payment.p_item
+
+    def expensive_actions_allowed(self, type=None, request=None):
+        """Tell, whether expensive actions are currently allowed.
+        Check system load/health (or other external circumstances) and
+        locally set values to see, whether expensive actions should be
+        allowed (`True`) or better avoided (`False`).
+        Use this to allow or forbid exports, report generations, or
+        similar actions.
+        """
+        max_values = self.SYSTEM_MAX_LOAD
+        for (key, func) in (
+            ('swap-mem', psutil.swap_memory),
+            ('virt-mem', psutil.virtual_memory),
+            ):
+            max_val = max_values.get(key, None)
+            if max_val is None:
+                continue
+            mem_val = func()
+            if isinstance(max_val, float):
+                # percents
+                if max_val < 0.0:
+                    max_val = 100.0 + max_val
+                if mem_val.percent > max_val:
+                    return False
+            else:
+                # number of bytes
+                if max_val < 0:
+                    max_val = mem_val.total + max_val
+                if mem_val.used > max_val:
+                    return False
+        return True
+
+    def export_disabled_message(self):
+        export_disabled_message = grok.getSite()[
+            'configuration'].export_disabled_message
+        if export_disabled_message:
+            return export_disabled_message
+        return None
+
+    def format_float(self, value, prec):
+        # >>> 4.6 * 100
+        # 459.99999999999994
+        value = decimal.Decimal(str(value))
+        # cut floating point value
+        value = int(pow(10, prec)*value) / (1.0*pow(10, prec))
+        return '{:{width}.{prec}f}'.format(value, width=0, prec=prec)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/webservices.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/webservices.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/webservices.py	(revision 16045)
@@ -0,0 +1,110 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+
+import grok
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.payments.interfaces import IPayer
+from waeup.kofa.interfaces import IUniversity, academic_sessions_vocab
+
+class PaymentDataWebservice(grok.View):
+    """A simple webservice to publish payment and payer details on request from
+    accepted IP addresses without authentication.
+
+    """
+    grok.context(IUniversity)
+    grok.name('paymentrequest')
+    grok.require('waeup.Public')
+
+    #ACCEPTED_IP = ('174.36.230.28', )
+    ACCEPTED_IP = ('127.0.0.1', )
+
+    def update(self, P_ID=None):
+        if P_ID == None:
+            self.output = '-1'
+            return
+        real_ip = self.request.get('HTTP_X_FORWARDED_FOR', None)
+        if real_ip:
+            self.context.logger.info('PaymentDataWebservice called: %s' % real_ip)
+        if real_ip and self.ACCEPTED_IP:
+            if real_ip not in  self.ACCEPTED_IP:
+                self.output = '-4'
+                return
+        cat = getUtility(ICatalog, name='payments_catalog')
+        results = list(
+            cat.searchResults(p_id=(P_ID, P_ID), p_state=('paid', 'paid')))
+        if len(results) != 1:
+            self.output = '-1'
+            return
+        try:
+            owner = IPayer(results[0])
+            full_name = owner.display_fullname
+            reg_number = owner.reg_number
+            matric_number = owner.matric_number
+            faculty = owner.faculty
+            department = owner.department
+        except (TypeError, AttributeError):
+            self.output = '-3'
+            return
+        amount = results[0].amount_auth
+        payment_category = results[0].category
+        payment_item = results[0].p_item
+        academic_session = academic_sessions_vocab.getTerm(
+            results[0].p_session).title
+        self.output = (
+            'FULL_NAME=%s&' +
+            'FACULTY=%s&' +
+            'DEPARTMENT=%s&' +
+            'PAYMENT_ITEM=%s&' +
+            'PAYMENT_CATEGORY=%s&' +
+            'ACADEMIC_SESSION=%s&' +
+            'MATRIC_NUMBER=%s&' +
+            'REG_NUMBER=%s&' +
+            'FEE_AMOUNT=%s') % (full_name, faculty,
+            department, payment_item, payment_category,
+            academic_session, matric_number, reg_number, amount)
+        return
+
+    def render(self):
+        return self.output
+
+class XMLRPCPermission(grok.Permission):
+    """Permission for using XMLRPC functions.
+    """
+    grok.name('waeup.xmlrpc')
+
+class XMLRPCUsers1(grok.Role):
+    """Usergroup 1
+    """
+    grok.name('waeup.xmlrpcusers1')
+    grok.title('XMLRPC Users Group 1')
+    grok.permissions('waeup.xmlrpc',)
+
+class UniversityXMLRPC(grok.XMLRPC):
+    """XMLRPC webservices for KOFA portals.
+
+    Please note, that XMLRPC does not support real keyword arguments
+    but positional arguments only.
+    """
+    grok.context(IUniversity)
+
+    @grok.require('waeup.Public')
+    def xmlrpc_api_version(self):
+        """Return the current API version for XMLRPC clients.
+        """
+        return u'0.1'
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/__init__.py	(revision 16045)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for Kofa.
+"""
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datetimewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datetimewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datetimewidget.py	(revision 16045)
@@ -0,0 +1,89 @@
+## $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
+##
+"""Datetimewidgets that are compatible with pytz.
+"""
+import datetime
+import pytz
+from zope.component import queryUtility
+from zope.datetime import parseDatetimetz
+from zope.datetime import DateTimeError
+from zope.formlib.i18n import _
+from zope.formlib.interfaces import ConversionError
+from zope.formlib.textwidgets import DatetimeWidget
+from waeup.kofa.utils.helpers import to_timezone
+from waeup.kofa.interfaces import IKofaUtils
+
+class _DummyUtils(object):
+    tzinfo = pytz.utc
+
+# A fallback, if no IKofaUtils can be found
+_DUMMY_UTILS = _DummyUtils()
+
+class PytzDatetimeWidget(DatetimeWidget):
+    """pytz-conform, non-ambigous UTC datetimes.
+
+    While the standard formlib datetime widget makes use of
+    zope.datetime and creates also not too reliable (or ambigous)
+    datetimes based on local servertime, this widget provides
+    non-ambigous UTC datetimes with a pytz timezone (pytz.utc).
+
+    Using this widget for datetime data we always get clean UTC
+    datetimes compatible with other Python packages (as pytz is more
+    wide-spread than zope.datetime).
+
+    For datetimes in other timezones we compute the correct UTC value
+    and store this. A way to help making sure, only UTC-based values
+    go into the DB.
+
+    For datetimes without any timezone set, we interpret the input to
+    be meant as local app-time. I.e. if application TZ is
+    ``Africa/Lagos``, we assume that a string like '2012-02-01 12:13'
+    (apparently not providing TZ info) was meant as 12:13 h Lagos
+    time.
+
+    From zope.datetime, however, we save the parser abilities to
+    interpret even bizarre entered data as some datetime.
+    """
+    def _toFieldValue(self, string):
+        """Turn string into a UTC-based datetime.
+
+        The TZ data is guaranteed to be pytz.utc.
+
+        """
+        # In import files we can use the hash symbol at the end of a
+        # datetime and date strings to avoid annoying automatic date
+        # transformation by Excel or Calc.
+        string = string.strip('#')
+        if string == self._missing:
+            return self.context.missing_value
+        else:
+            try:
+                # Different to original implementation we do not
+                # automatically request local server time if no TZ was
+                # set in `string`. In this case we want a datetime with
+                # tzinfo set to `None` instead.
+                value = parseDatetimetz(string, local=False)
+            except (DateTimeError, ValueError, IndexError), v:
+                raise ConversionError(_("Invalid datetime data"), v)
+
+        if not isinstance(value, datetime.datetime):
+            return value
+        if value.tzinfo is None:
+            utils = queryUtility(IKofaUtils, default=_DUMMY_UTILS)
+            value = utils.tzinfo.localize(value)
+        return value.astimezone(pytz.utc)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/datewidget.py	(revision 16045)
@@ -0,0 +1,248 @@
+## $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
+##
+"""
+A datewidget with customizable date format.
+"""
+import pytz
+from datetime import datetime
+from zope.component import queryUtility
+from zope.formlib.interfaces import ConversionError, IDisplayWidget
+from zope.formlib.textwidgets import DateWidget, DateDisplayWidget, escape
+from zope.formlib.widget import renderElement, CustomWidgetFactory
+from zope.interface import implements
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.utils.helpers import to_timezone
+
+
+#: A dictionary of supported date formats.
+#:
+#: The following formats are supported:
+#:
+#: ``iso``
+#:    ISO format ``YYYY-MM-DD``
+#: ``le``
+#:    little endian with slashes: ``DD/MM/YYYY``
+#: ``de``
+#:    german date format: ``DD.MM.YYYY``
+#: ``us``
+#:    middle endian format common in the U.S.: ``MM/DD/YYYY``
+#:
+#: Furthermore we support for input widgets an additional year
+#: marker. Input date widgets with this marker provide also a year
+#: selector, handy for dates of birth etc.
+#:
+#: The year-supporting formats are similar to the basic versions above:
+#:
+#: ``iso-year``
+#:    ISO format ``YYYY-MM-DD``
+#: ``le-year``
+#:    little endian with slashes: ``DD/MM/YYYY``
+#: ``de-year``
+#:    german date format: ``DD.MM.YYYY``
+#: ``us-year``
+#:    middle endian format common in the U.S.: ``MM/DD/YYYY``
+#:
+#: For date display widgets there is naturally no difference between a
+#: year and non-year setting (you can for instance use 'le' or 'le-year'
+#: with the same output).
+DATE_FORMATS = {
+    'iso': ('datepicker', '%Y-%m-%d'),
+    'le':  ('datepicker-le', '%d/%m/%Y'),
+    'de':  ('datepicker-de', '%d.%m.%Y'),
+    'us':  ('datepicker-us', '%m/%d/%Y'),
+    'iso-year': ('datepicker-year', '%Y-%m-%d'),
+    'le-year':  ('datepicker-le-year', '%d/%m/%Y'),
+    'de-year':  ('datepicker-de-year', '%d.%m.%Y'),
+    'us-year':  ('datepicker-us-year', '%m/%d/%Y'),
+    }
+
+#: a dict containing tuples (<FORMAT>, <SHOW_YEAR>) as keys and
+#: suitable CSS tags as values.
+FORMATS_BY_VALUE = dict(
+    [((val[1], 'year' in key), val[0]) for key, val in DATE_FORMATS.items()])
+
+class FormattedDateWidget(DateWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    If the widget is bound to a schema field with respective
+    attributes, it reads its `show_year` and `date_format` attributes
+    (see waeup.kofa.schema.FormattedDate for an example) and sets a
+    CSS tag according to these values.
+
+    The widget also accepts ISO format as a fallback, even if a
+    different format was set. This should help with imports.
+
+    This is an input widget.
+    """
+    date_format = '%Y-%m-%d'
+    show_year = False
+
+    def __init__(self, context, request, *args, **kw):
+        # try to grab date_format and show_year from bound schema field.
+        date_format = getattr(context, 'date_format', self.date_format)
+        if date_format is not None:
+            self.date_format = date_format
+        self.show_year = getattr(context, 'show_year', self.show_year)
+        # add css class determined by date_format and show_year
+        css_cls = FORMATS_BY_VALUE.get((self.date_format, self.show_year), '')
+        self.cssClass = ' '.join([self.cssClass, css_cls]).strip()
+        return super(FormattedDateWidget, self).__init__(
+            context, request, *args, **kw)
+
+    def _toFieldValue(self, input):
+        # In import files we can use the hash symbol at the end of a
+        # date string to avoid annoying automatic date transformation
+        # by Excel or Calc
+        input = input.strip('#')
+        if input == self._missing:
+            return self.context.missing_value
+        else:
+            try:
+                value = datetime.strptime(input, self.date_format)
+            except (ValueError, IndexError), v:
+                try:
+                    # Try ISO format as fallback.
+                    # This is needed for instance during imports.
+                    value = datetime.strptime(
+                        input, FormattedDateWidget.date_format)
+                except (ValueError, IndexError), v:
+                    raise ConversionError("Invalid datetime data", v)
+        return value.date()
+
+    def _toFormValue(self, value):
+        if value:
+            try:
+                value = value.strftime(self.date_format)
+            except ValueError:
+                return value
+        return value
+
+
+class FormattedDateDisplayWidget(DateDisplayWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    This is a display widget.
+
+    It can also be used for displaying datetimes. If used to display a
+    datetime (not a date), the widget returns local datetime with
+    timezone set according to KofaUtils.
+    """
+    date_format = '%Y-%m-%d'
+    show_year = False
+
+    implements(IDisplayWidget)
+
+    def __init__(self, context, request, *args, **kw):
+        # try to grab date_format and show_year from bound schema field.
+        date_format = getattr(context, 'date_format', self.date_format)
+        if date_format is not None:
+            self.date_format = date_format
+        self.show_year = getattr(context, 'show_year', self.show_year)
+        return super(FormattedDateDisplayWidget, self).__init__(
+            context, request, *args, **kw)
+
+    def __call__(self):
+        if self._renderedValueSet():
+            content = self._data
+        else:
+            content = self.context.default
+        if content == self.context.missing_value:
+            return ""
+        if isinstance(content, datetime):
+            # shift value to local timezone
+            tz = pytz.utc
+            utils = queryUtility(IKofaUtils)
+            if utils is not None:
+                tz = utils.tzinfo
+            content = to_timezone(content, tz)
+        try:
+            content = content.strftime(self.date_format)
+        except ValueError:
+            return None
+        return renderElement("span", contents=escape(content),
+                             cssClass=self.cssClass)
+
+class DateLEWidget(FormattedDateWidget):
+    date_format = '%d/%m/%Y'
+
+class DateDEWidget(FormattedDateWidget):
+    date_format = '%d.%m.%Y'
+
+class DateUSWidget(FormattedDateWidget):
+    date_format = '%m/%d/%Y'
+
+class DateLEDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%d/%m/%Y'
+
+class DateDEDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%d.%m.%Y'
+
+class DateUSDisplayWidget(FormattedDateDisplayWidget):
+    date_format = '%m/%d/%Y'
+
+def FriendlyDateWidget(format):
+    """Get a friendly date input widget for `format`.
+
+    This widget is suitable for edit and add forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY).
+
+    Friendly date widgets are rendered with a specialized CSS tag for
+    enabling JavaScript datepickers.
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    return CustomWidgetFactory(
+        FormattedDateWidget,
+        cssClass=css_class,
+        date_format=date_format)
+
+def FriendlyDateDisplayWidget(format):
+    """Get a friendly date display widget for `format`.
+
+    This widget is suitable for display forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY).
+
+    This widget is not rendered with a specialized CSS tag for
+    enabling JavaScript datepickers. `css_class` is ignored which means
+    there is nor difference between e.g. ``le`` and ``le-year``.`
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    return CustomWidgetFactory(
+        FormattedDateDisplayWidget,
+        date_format=date_format)
+
+def FriendlyDatetimeDisplayWidget(format):
+    """Get a friendly datetime display widget for `format`.
+
+    This widget is suitable for display forms.
+
+    Valid `format` values are the keys of `DATE_FORMATS`
+    dict. Default is ``le`` (little endian; DD/MM/YYYY %H:%M:%S).
+
+    This widget is not rendered with a specialized CSS tag for
+    enabling JavaScript datepickers. `css_class` is ignored which means
+    there is no difference between e.g. ``le`` and ``le-year``.`
+    """
+    css_class, date_format = DATE_FORMATS.get(format, DATE_FORMATS['le'])
+    datetime_format = date_format + ' %H:%M:%S'
+    return CustomWidgetFactory(
+        FormattedDateDisplayWidget,
+        date_format=datetime_format)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/interfaces.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/interfaces.py	(revision 16045)
@@ -0,0 +1,21 @@
+## $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
+##
+"""Interfaces for widgets.
+"""
+from zope.interface import Interface
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.pt	(revision 16045)
@@ -0,0 +1,11 @@
+<span tal:repeat="widget context/subwidgets">
+  <span tal:define="error widget/error"
+    tal:replace="structure error" tal:condition="error" />
+  <span class="hint" tal:condition="widget/hint">
+    <span tal:content="widget/hint" />:
+  </span>
+  <span tal:replace="structure widget" />
+  <span tal:condition="python: context.display and widget.hint">
+    <br />
+  </span>
+</span>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/objectwidget.py	(revision 16045)
@@ -0,0 +1,78 @@
+## $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
+##
+"""A widget to display IObject instances in forms.
+"""
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.interfaces import IDisplayWidget
+from zope.formlib.objectwidget import ObjectWidget, ObjectWidgetView
+from zope.formlib.utility import setUpWidgets
+from zope.interface import implements, implementsOnly
+from zope.schema import getFieldNamesInOrder
+
+class KofaObjectWidgetView(ObjectWidgetView):
+    template = ViewPageTemplateFile('objectwidget.pt')
+
+class KofaObjectWidget(ObjectWidget):
+
+    display = False
+
+    def __init__(self, context, request, factory, **kw):
+        #super(ResultsEntryWidget, self).__init__(context, request)
+        super(ObjectWidget, self).__init__(context, request)
+
+        # define view that renders the widget
+        self.view = self._getView(request)
+
+        # factory used to create content that this widget (field)
+        # represents
+        self.factory = factory
+
+        # handle foo_widget specs being passed in
+        self.names = getFieldNamesInOrder(self.context.schema)
+        for k, v in kw.items():
+            if k.endswith('_widget'):
+                setattr(self, k, v)
+
+        # set up my subwidgets
+        self._setUpWidgets()
+
+    def subwidgets(self):
+        result = [self.getSubWidget(name) for name in self.names]
+        return result
+
+    def _setUpWidgets(self):
+        return self._setUpEditWidgets()
+
+    def _getView(self, request):
+        return KofaObjectWidgetView(self, request)
+
+
+class KofaObjectDisplayWidget(KofaObjectWidget):
+
+    implementsOnly(IDisplayWidget)
+
+    display = True
+
+    def _setUpDisplayWidgets(self):
+        # subwidgets need a new name
+        setUpWidgets(self, self.context.schema, IDisplayWidget,
+                         prefix=self.name, names=self.names,
+                         context=self.context)
+
+    def _setUpWidgets(self):
+        return self._setUpDisplayWidgets()
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/orderedSelectionList.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/orderedSelectionList.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/orderedSelectionList.pt	(revision 16045)
@@ -0,0 +1,194 @@
+<script type="text/javascript">
+
+function moveItems(from, to)
+  {
+  // shortcuts for selection fields
+  var src = document.getElementById(from);
+  var tgt = document.getElementById(to);
+
+  if (src.selectedIndex == -1) selectionError();
+  else
+    {
+    // iterate over all selected items
+    // --> attribute "selectedIndex" doesn't support multiple selection.
+    // Anyway, it works here, as a moved item isn't selected anymore,
+    // thus "selectedIndex" indicating the "next" selected item :)
+    while (src.selectedIndex > -1)
+      if (src.options[src.selectedIndex].selected)
+        {
+        // create a new virtal object with values of item to copy
+        temp = new Option(src.options[src.selectedIndex].text,
+                      src.options[src.selectedIndex].value);
+        // append virtual object to targe
+        tgt.options[tgt.length] = temp;
+        // want to select newly created item
+        temp.selected = true;
+        // delete moved item in source
+        src.options[src.selectedIndex] = null;
+      }
+    }
+  }
+
+// move item from "from" selection to "to" selection
+function from2to(name)
+  {
+  moveItems(name+".from", name+".to");
+  copyDataForSubmit(name);
+  }
+
+// move item from "to" selection back to "from" selection
+function to2from(name)
+  {
+  moveItems(name+".to", name+".from");
+  copyDataForSubmit(name);
+  }
+
+function swapFields(a, b)
+  {
+  // swap text
+  var temp = a.text;
+  a.text = b.text;
+  b.text = temp;
+  // swap value
+  temp = a.value;
+  a.value = b.value;
+  b.value = temp;
+  // swap selection
+  temp = a.selected;
+  a.selected = b.selected;
+  b.selected = temp;
+  }
+
+// move selected item in "to" selection one up
+function moveUp(name)
+  {
+  // shortcuts for selection field
+  var toSel = document.getElementById(name+".to");
+
+  if (toSel.selectedIndex == -1)
+      selectionError();
+  else if (toSel.options[0].selected)
+      alert("Cannot move further up!");
+  else for (var i = 0; toSel.length > i; i++)
+    if (toSel.options[i].selected)
+      {
+      swapFields(toSel.options[i-1], toSel.options[i]);
+      copyDataForSubmit(name);
+      }
+  }
+
+// move selected item in "to" selection one down
+function moveDown(name)
+  {
+    // shortcuts for selection field
+    var toSel = document.getElementById(name+".to");
+
+    if (toSel.selectedIndex == -1) {
+        selectionError();
+    } else if (toSel.options[toSel.length-1].selected) {
+        alert("Cannot move further down!");
+    } else {
+      for (var i = toSel.length-1; i >= 0; i--) {
+        if (toSel.options[i].selected) {
+          swapFields(toSel.options[i+1], toSel.options[i]);
+        }
+      }
+      copyDataForSubmit(name);
+    }
+  }
+
+// copy each item of "toSel" into one hidden input field
+function copyDataForSubmit(name)
+  {
+  // shortcuts for selection field and hidden data field
+  var toSel = document.getElementById(name+".to");
+  var toDataContainer = document.getElementById(name+".toDataContainer");
+
+  // delete all child nodes (--> complete content) of "toDataContainer" span
+  while (toDataContainer.hasChildNodes())
+      toDataContainer.removeChild(toDataContainer.firstChild);
+
+  // create new hidden input fields - one for each selection item of
+  // "to" selection
+  for (var i = 0; toSel.options.length > i; i++)
+    {
+    // create virtual node with suitable attributes
+    var newNode = document.createElement("input");
+    var newAttr = document.createAttribute("name");
+    newAttr.nodeValue = name;
+    newNode.setAttributeNode(newAttr);
+
+    newAttr = document.createAttribute("value");
+    newAttr.nodeValue = toSel.options[i].value;
+    newNode.setAttributeNode(newAttr);
+
+    // actually append virtual node to DOM tree
+    toDataContainer.appendChild(newNode);
+    }
+  }
+
+// error message for missing selection
+function selectionError()
+  {alert("Must select something!")}
+
+</script>
+
+<table border="0" class="ordered-selection-field">
+  <tr>
+    <td>
+      <select id="from" name="from" size="5" multiple="multiple"
+          tal:attributes="name string:${view/name}.from;
+                          id string:${view/name}.from;
+                          size view/size"
+                          >
+        <option tal:repeat="entry view/choices"
+                tal:attributes="value entry/value"
+                tal:content="entry/text" i18n:translate=""/>
+      </select>
+    </td>
+    <td>
+      <button class="btn btn-default" name="from2toButton" type="button" value=" -&gt;"
+          onclick="javascript:from2to()"
+          tal:attributes="onClick string:javascript:from2to('${view/name}')"
+          ><img src="/static/img/arrow-right.png" alt="Add" /></button>
+      <br />
+      <button class="btn btn-default" name="to2fromButton" type="button"
+          onclick="javascript:to2from()"
+          tal:attributes="onClick string:javascript:to2from('${view/name}')"
+          ><img src="/static/img/arrow-left.png" alt="Remove" /></button>
+    </td>
+    <td>
+      <select id="to" name="to" size="5" multiple="multiple"
+          tal:attributes="name string:${view/name}.to;
+                          id string:${view/name}.to;
+                          size view/size">
+        <option tal:repeat="entry view/selected"
+                tal:attributes="value entry/value"
+                tal:content="entry/text" i18n:translate=""/>
+      </select>
+      <input name="foo-empty-marker" type="hidden"
+        tal:attributes="name string:${view/name}-empty-marker"/>
+      <span id="toDataContainer" style="display: none"
+            tal:attributes="id string:${view/name}.toDataContainer">
+        <script type="text/javascript" tal:content="string:
+          copyDataForSubmit('${view/name}');">
+          // initial copying of field "field.to" --> "field"
+          copyDataForSubmit("<i tal:replace="${view/name}"/>");
+        </script>
+      </span>
+    </td>
+    <td>
+      <button class="btn btn-default"
+          name="upButton" type="button" value="^"
+          onclick="javascript:moveUp()"
+          tal:attributes="onClick string:javascript:moveUp('${view/name}')"
+          ><img src="/static/img/arrow-up.png" alt="Up" /></button>
+      <br />
+      <button class="btn btn-default"
+          name="downButton" type="button" value="v"
+          onclick="javascript:moveDown()"
+          tal:attributes="onClick string:javascript:moveDown('${view/name}')"
+          ><img src="/static/img/arrow-down.png" alt="Down" /></button>
+    </td>
+  </tr>
+</table>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/overrides.zcml
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/overrides.zcml	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/overrides.zcml	(revision 16045)
@@ -0,0 +1,65 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <adapter
+      for="zope.schema.interfaces.IDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.datewidget.FormattedDateWidget"
+      permission="zope.Public"
+      />
+
+  <adapter
+      for="waeup.kofa.schema.interfaces.IFormattedDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.datewidget.DateLEWidget"
+      permission="zope.Public"
+      />
+
+  <adapter
+      for="waeup.kofa.schema.interfaces.IFormattedDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.IDisplayWidget"
+      factory="waeup.kofa.widgets.datewidget.DateLEDisplayWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register edit widget for PhoneNumbers -->
+  <adapter
+      for="waeup.kofa.schema.interfaces.IPhoneNumber
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.phonewidget.PhoneWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register pytz-aware edit widget for datetimes -->
+  <adapter
+      for="zope.schema.interfaces.IDatetime
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.datetimewidget.PytzDatetimeWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Register KofaSourceOrderedMultiSelectWidget for IterableSource -->
+  <adapter
+      for="zope.schema.interfaces.IList
+           zope.schema.interfaces.IIterableSource
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.source.KofaSourceOrderedMultiSelectWidget"
+      permission="zope.Public"
+      />
+      
+  <!-- Register KofaSourceOrderedMultiSelectWidget for IVocabularyTokenized -->
+  <adapter
+      for="zope.schema.interfaces.IList
+           zope.schema.interfaces.IVocabularyTokenized
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.kofa.widgets.source.KofaOrderedMultiSelectWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/phonewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/phonewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/phonewidget.py	(revision 16045)
@@ -0,0 +1,171 @@
+"""A phone number widget.
+
+This widget is an input widget (not made for display forms but for
+edit and add forms).
+
+It can be used for :class:`zope.schema.TextLine` fields but has to be
+requested by the form manually (otherwise the regular TextLine widget
+will be used for rendering).
+
+If you use the PhoneWidget for rendering regular TextLine attributes
+(preferably in edit forms or add forms), the phone number is displayed
+by three input fields representing the international code, the area
+code and the extension line.
+
+When the entered input is stored with a context object, it is stored
+as a single unicode string with the numbers divided by single hyphen.
+
+So, input <+12>, <111>, <444> becomes the string ``'+12-111-444'`` for
+the context object.
+"""
+import grok
+import re
+from zope.component import queryUtility
+from zope.formlib.interfaces import MissingInputError, InputErrors
+from zope.interface import Interface
+from zope.formlib.textwidgets import (
+    TextWidget, renderElement, ConversionError)
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.utils import KofaUtils
+
+RE_INT_PREFIX = re.compile('^\+\d+')
+RE_NUMBERS = re.compile('^\d+$')
+RE_NUMBERS_AND_HYPHENS = re.compile('^[\d\-]+$')
+
+class PhoneWidget(TextWidget):
+
+    subwidget_names = ('country', 'area', 'ext')
+    cssClass = 'phone-input'
+
+    def _renderPrefixWidget(self, value):
+        prefix_func = getattr(
+            queryUtility(IKofaUtils), 'sorted_phone_prefixes',
+            KofaUtils.sorted_phone_prefixes)
+        options = []
+        for ptitle, pval in prefix_func(request=self.request):
+            selected = ''
+            if value == pval:
+                selected = ' selected="selected" '
+            options.append(
+                '<option value="%s"%s>%s</option>' % (pval, selected, ptitle))
+        options = '\n'.join(options)
+        return '<select id="%s" name="%s" size="1" class="%s">\n%s\n</select>' % (
+            '%s.%s' % (self.name, 'country'),
+            '%s.%s' % (self.name, 'country'),
+            self.cssClass,
+            options)
+
+    def __call__(self):
+        value = self._getFormValue()
+        if value is None or value == self.context.missing_value:
+            value = ''
+        if len(value.split('-')) < 2:
+            value = '--' + value
+        subvalues = value.split('-', 2)
+
+        kwargs = {'type': self.type,
+                  'name': self.name,
+                  'id': self.name,
+                  'value': value,
+                  'cssClass': self.cssClass,
+                  'style': self.style,
+                  'size': self.displayWidth,
+                  'extra': self.extra}
+        if self.displayMaxWidth:
+            kwargs['maxlength'] = self.displayMaxWidth # TODO This is untested.
+        fields = []
+        for num, subname in enumerate(self.subwidget_names):
+            if num == 0:
+                select = self._renderPrefixWidget(subvalues[num])
+                fields.append(select)
+                continue
+                print select
+            kwargs.update(name = '%s.%s' % (self.name, subname))
+            kwargs.update(id=kwargs['name'])
+            kwargs.update(cssClass = '%s %s' % ('', self.cssClass))
+            kwargs.update(value = subvalues[num])
+            fields.append(renderElement(self.tag, **kwargs))
+        return '-'.join(fields)
+
+    def _getFormInput(self):
+        """Returns current form input.
+
+        The value returned must be in a format that can be used as the 'input'
+        argument to `_toFieldValue`.
+
+        The default implementation returns the form value that corresponds to
+        the widget's name. Subclasses may override this method if their form
+        input consists of more than one form element or use an alternative
+        naming convention.
+        """
+        result = '-'.join(
+            [self.request.get('%s.%s' % (self.name, name), '')
+             for name in self.subwidget_names])
+        return result
+
+    def _toFieldValue(self, input):
+        """Check value entered in form further.
+
+        Raises ConversionError if values entered contain non-numbers.
+
+        For the extension line we silently allow slashes as well.
+        """
+        # In import files we can use the hash symbol at the end of a
+        # date string to avoid annoying automatic number transformation
+        # by Excel or Calc
+        input = input.strip('#')
+        result = super(PhoneWidget, self)._toFieldValue(input)
+        parts = input.split('-', 2)
+        if '' in parts and self.context.required:
+            raise ConversionError(
+                _("Empty phone field(s)."), MissingInputError(
+                    self.name, self.label, None))
+        if parts[0] != '' and not RE_INT_PREFIX.match(parts[0]):
+            raise ConversionError(
+                _("Int. prefix requires format '+NNN'"),
+                ValueError('invalid international prefix'))
+        # Make sure there are only numbers in parts 1..N. We do not allow
+        # dashes in last field any more.
+        errors = [(x != '' and not RE_NUMBERS.match(x)) for x in parts[1:]]
+        error = True in errors
+        if error:
+            raise ConversionError(
+                _("Phone numbers may contain numbers only."),
+                ValueError('non numbers in phone number'))
+        # We consider also values ending with empty ending as missing
+        # values.  An 'empty ending' is a form where the fields
+        # following the prefix are all empty.  This means that any
+        # prefix setting in the form will switch back to default upon
+        # submit if no further phone fields are filled.  As advantage
+        # we get only valid phone numbers or missing value.
+        empty_ending = '-'*(len(parts) - 1)
+        if result in ('', None) or result.endswith(empty_ending):
+            result = self.context.missing_value
+        return result
+
+    def _getFormValue(self):
+        """Returns a value suitable for use in an HTML form.
+
+        Detects the status of the widget and selects either the input value
+        that came from the request, the value from the _data attribute or the
+        default value.
+        """
+        try:
+            input_value = self._getCurrentValueHelper()
+        except InputErrors:
+            form_value = '-'.join(
+                [self.request.form.get('%s.%s' % (self.name, name), '')
+                 for name in self.subwidget_names]
+                )
+        else:
+            form_value = self._toFormValue(input_value)
+        return form_value
+
+    def hasInput(self):
+        """A phone widget has input if all three subfields have input.
+        """
+        for name in self.subwidget_names:
+            if '%s.%s' % (self.name, name) not in self.request.form:
+                return False
+        return True
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.pt
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.pt	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.pt	(revision 16045)
@@ -0,0 +1,32 @@
+<table i18n:domain="waeup.kofa">
+  <tr tal:repeat="widget view/widgets">
+    <td>
+      <input class="editcheck" type="checkbox"
+             tal:attributes="
+                 name string:${view/name}.remove_${repeat/widget/index}"
+             tal:condition="view/need_delete" />
+    </td>
+    <td>
+      <input tal:replace="structure widget" />
+    </td>
+  </tr>
+  <tr>
+    <td colspan="2">
+      <p class="hint" i18n:translate="">
+      Don't forget to save the form after adding or deleting items.
+      </p>
+      <button type="submit"
+             class="btn btn-default"
+             tal:condition="view/need_add"
+             tal:attributes="name string:${view/name}.add"
+             tal:content="view/addButtonLabel" />
+      <button type="submit" class="btn btn-default"
+             tal:condition="view/need_delete"
+             tal:attributes="name string:${view/name}.remove"
+             i18n:translate="remove-selected-items">Remove selected items</button>
+      <a id="kofa-scroll-target"
+         tal:condition="view/_kofa_seq_len_changed" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/sequencewidget.py	(revision 16045)
@@ -0,0 +1,108 @@
+## $Id$
+##
+## Copyright (C) 2012 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
+##
+"""An improved sequence widget.
+
+We provide a sequence widget that better suits our needs layout-wise
+than the default implementation from zope.formlib.
+"""
+import grok
+from zope.i18n import translate
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.formlib.widgets import (
+    ListSequenceWidget, SequenceDisplayWidget)
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.schema.interfaces import IField, IList
+
+
+class KofaSequenceWidget(ListSequenceWidget):
+    """A sequence widget for lists.
+
+    This is basically a plain copy from zope.formlib. We have,
+    however, the possibility to tweak the attached template.
+    """
+    template = ViewPageTemplateFile('sequencewidget.pt')
+    _kofa_seq_len_changed = False
+
+    def _generateSequence(self):
+        result = super(KofaSequenceWidget, self)._generateSequence()
+        if ((self.name + '.add' in self.request.form) or
+            (self.name + '.remove' in self.request.form)):
+            self._kofa_seq_len_changed = True
+        return result
+
+
+class KofaSequenceDisplayWidget(SequenceDisplayWidget):
+    """A sequence widget for lists.
+
+    This is basically a plain copy from zope.formlib. We have,
+    however, the possibility to tweak html tags.
+    """
+
+    tag = None
+    itemTag = 'div'
+
+    def __call__(self):
+        """Patch for the orginal __call__ method.
+
+        The orginal __call__ method doesn't call
+        the translate function properly.
+        """
+
+        # get the data to display:
+        if self._renderedValueSet():
+            data = self._data
+        else:
+            data = self.context.get(self.context.context)
+
+        # deal with special cases:
+        if data == self.context.missing_value:
+            return translate(self._missingValueMessage, context=self.request)
+        data = list(data)
+        if not data:
+            return translate(self._emptySequenceMessage, context=self.request)
+
+        parts = []
+        for i, item in enumerate(data):
+            widget = self._getWidget(i)
+            widget.setRenderedValue(item)
+            s = widget()
+            if self.itemTag:
+                s = "<%s>%s</%s>" % (self.itemTag, s, self.itemTag)
+            parts.append(s)
+        contents = "\n".join(parts)
+        if self.tag:
+            contents = "\n%s\n" % contents
+            contents = renderElement(self.tag,
+                                     cssClass=self.cssClass,
+                                     extra=self.extra,
+                                     contents=contents)
+        return contents
+
+
+# Register our sequence widgets as default for lists.
+@grok.adapter(IList, IField, IBrowserRequest)
+@grok.implementer(IInputWidget)
+def seq_input_widget(obj, field, req, *args, **kw):
+    return KofaSequenceWidget(obj, field, req, *args, **kw)
+
+
+@grok.adapter(IList, IField, IBrowserRequest)
+@grok.implementer(IDisplayWidget)
+def seq_display_widget(obj, field, req, *args, **kw):
+    return KofaSequenceDisplayWidget(obj, field, req, *args, **kw)
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/source.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/source.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/source.py	(revision 16045)
@@ -0,0 +1,40 @@
+## $Id: source.py 12506 2015-01-22 09:54:57Z henrik $
+##
+## Copyright (C) 2015 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
+##
+"""Improved source widgets.
+
+"""
+
+from zope.browserpage import ViewPageTemplateFile
+from zope.formlib.source import SourceOrderedMultiSelectWidget
+from zope.formlib.itemswidgets import OrderedMultiSelectWidget
+
+class KofaSourceOrderedMultiSelectWidget(SourceOrderedMultiSelectWidget):
+    """A multi-selection widget with ordering support."""
+
+    template = ViewPageTemplateFile('orderedSelectionList.pt')
+
+    def __call__(self):
+        return self.template()
+
+class KofaOrderedMultiSelectWidget(OrderedMultiSelectWidget):
+    """A multi-selection widget with ordering support."""
+
+    template = ViewPageTemplateFile('orderedSelectionList.pt')
+
+    def __call__(self):
+        return self.template()
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/__init__.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/__init__.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/__init__.py	(revision 16045)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datetimewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datetimewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datetimewidget.py	(revision 16045)
@@ -0,0 +1,102 @@
+# Tests for datetimewidgets
+import datetime
+import pytz
+from zope import schema
+from zope.component import getGlobalSiteManager
+from zope.formlib import form
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from zope.formlib.tests.test_functional_textwidget import(
+    FunctionalWidgetTestCase,)
+from zope.formlib.textwidgets import TextWidget
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyClass, verifyObject
+from zope.publisher.browser import TestRequest
+from waeup.kofa.interfaces import IKofaUtils
+from waeup.kofa.widgets.datetimewidget import PytzDatetimeWidget
+
+class IContent(Interface):
+    my_dt = schema.Datetime(
+        title = u'A datetime.',
+        )
+
+class Content(object):
+    implements(IContent)
+    my_dt = None
+
+class SampleForm(form.EditForm):
+    form_fields = form.fields(IContent)
+    form_fields['my_dt'].custom_widget = PytzDatetimeWidget
+
+class FakeUtils(object):
+    # Fake app-wide set timezone.
+    implements(IKofaUtils)
+    tzinfo = pytz.timezone('America/Sao_Paulo')
+
+
+class PytzDatetimeWidgetTests(FunctionalWidgetTestCase):
+
+    widgets = [
+        (schema.interfaces.IDatetime, TextWidget),
+        ]
+
+    def setUp(self):
+        super(PytzDatetimeWidgetTests, self).setUp()
+        self.gsm = getGlobalSiteManager()
+        self.utils = FakeUtils()
+        self.gsm.registerUtility(self.utils)
+        return
+
+    def tearDown(self):
+        super(PytzDatetimeWidgetTests, self).tearDown()
+        self.gsm.unregisterUtility(self.utils)
+        return
+
+    def test_iface(self):
+        # make sure we fullfill interface contracts
+        obj = PytzDatetimeWidget(IContent['my_dt'], TestRequest())
+        verifyClass(IInputWidget, PytzDatetimeWidget)
+        verifyObject(IInputWidget, obj)
+        return
+
+    def test_to_value(self):
+        # we get always UTC-based values
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2012-01-02 12:10:19 GMT+1'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        self.assertEqual( content.my_dt, datetime.datetime(
+            2012, 1, 2, 11, 10, 19, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
+
+    def test_export_values(self):
+        # make sure we understand values as exported
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2012-01-02 12:10:19+00:00'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        self.assertEqual( content.my_dt, datetime.datetime(
+            2012, 1, 2, 12, 10, 19, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
+
+    def test_datetimes_wo_tz(self):
+        # Datetimes w/o tz are considered to be meant in app-wide timezone.
+        content = Content()
+        request = TestRequest()
+
+        request.form['form.my_dt'] = u'2011-11-11 11:11:11'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        # Sao Paulo was two hours back UTC on 2011-11-11
+        self.assertEqual(content.my_dt, datetime.datetime(
+            2011, 11, 11, 13, 11, 11, tzinfo=pytz.utc))
+        self.assertTrue(content.my_dt.tzinfo is pytz.utc)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_datewidget.py	(revision 16045)
@@ -0,0 +1,192 @@
+## $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
+##
+"""Formatted Date widget tests.
+"""
+import datetime
+import unittest
+import doctest
+from zope.schema import Date
+from zope.interface import Interface, implementer
+from zope.interface.verify import verifyClass
+from zope.publisher.browser import TestRequest
+
+from zope.formlib.tests.test_browserwidget import (
+    SimpleInputWidgetTest, BrowserWidgetTest, )
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from waeup.kofa.widgets.datewidget import (
+    FormattedDateWidget, FormattedDateDisplayWidget, )
+from zope.formlib.widgets import DateI18nWidget
+from zope.formlib.interfaces import ConversionError, WidgetInputError
+
+
+class FormattedDateWidgetTest(SimpleInputWidgetTest):
+    """Documents and tests the formatted date widget.
+
+        >>> verifyClass(IInputWidget, FormattedDateWidget)
+        True
+    """
+
+    _FieldFactory = Date
+    _WidgetFactory = FormattedDateWidget
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        # same as in base class but with a min value of date(1910, 1, 1)
+        field = self._FieldFactory(
+            __name__='foo', title=title, description=desc,
+            min=datetime.date(1910, 1, 1))
+        class ITestContent(Interface):
+            foo = field
+        @implementer(ITestContent)
+        class TestObject:
+            pass
+        self.content = TestObject()
+        field = ITestContent['foo']
+        self.field = field.bind(self.content)
+        request = TestRequest(HTTP_ACCEPT_LANGUAGE='ru')
+        request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(field, request)
+
+    def testRender(self):
+        super(FormattedDateWidgetTest, self).testRender(
+            datetime.date(2003, 3, 26),
+            ('type="text"', 'id="field.foo"', 'name="field.foo"',
+                'value="2003-03-26"'))
+
+    def testRenderCustomFormat(self):
+        self._widget.date_format = "%d/%m/%y"
+        super(FormattedDateWidgetTest, self).testRender(
+            datetime.datetime(2004, 3, 26, 12, 58, 59),
+            ('type="text"', 'id="field.foo"', 'name="field.foo"',
+                'value="26/03/04"'))
+
+    def testProperties(self):
+        self.assertEqual(self._widget.tag, 'input')
+        self.assertEqual(self._widget.type, 'text')
+        # By default the date format is ISO and the rendered CSS 'datepicker'
+        self.assertEqual(self._widget.cssClass, 'datepicker')
+        self.assertEqual(self._widget.extra, '')
+
+    def test_hasInput(self):
+        del self._widget.request.form['field.foo']
+        self.assertFalse(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u''
+        self.assertTrue(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u'2003-03-26'
+        self.assertTrue(self._widget.hasInput())
+
+    def test_getDefaultInputValue(self,
+            value=u'2004-03-26',
+            check_value=datetime.date(2004, 3, 26)):
+        self._widget.request.form['field.foo'] = u''
+        self.assertRaises(WidgetInputError, self._widget.getInputValue)
+        self._widget.request.form['field.foo'] = value
+        self.assertEquals(self._widget.getInputValue(), check_value)
+        self._widget.request.form['field.foo'] = u'abc'
+        self.assertRaises(ConversionError, self._widget.getInputValue)
+
+    def test_getCustomInputValue_de(self):
+        self._widget.date_format = "%d.%m.%y"
+        self.test_getDefaultInputValue(u'26.03.04')
+
+    def test_getCustomInputValue_de2(self):
+        self._widget.date_format = "%d.%m.%Y"
+        self.test_getDefaultInputValue(u'26.03.2004')
+
+    def test_getCustomInputValue_us(self):
+        self._widget.date_format = "%m/%d/%Y"
+        self.test_getDefaultInputValue(u'03/26/2004')
+
+    def test_minimal_value_respected(self):
+        # we did set up the date field to require dates >= 1900-01-01
+        request = TestRequest()
+        # setting a date > 1900-01-01 is okay
+        request.form['field.foo'] = '1912-03-27'
+        widget = FormattedDateWidget(self.field, request)
+        self.assertEqual(
+            widget.getInputValue(), datetime.date(1912, 3, 27))
+
+        # setting a date < 1900-01-01 will fail
+        request.form['field.foo'] = '1812-03-27'
+        widget = FormattedDateWidget(self.field, request)
+        self.assertRaises(
+            WidgetInputError,
+            widget.getInputValue)
+        # check the correct exception message
+        try:
+            widget.getInputValue()
+        except WidgetInputError as exc:
+            # just catch the exception
+            pass
+        exc = '%r' % exc  # turn exception into string
+        self.assertEqual(
+            exc,
+            "WidgetInputError('foo', u'Foo Title', "
+            "TooSmall(datetime.date(1812, 3, 27), datetime.date(1910, 1, 1)))"
+            )
+        return
+
+class FormattedDateDisplayWidgetTest(BrowserWidgetTest):
+    """The FormatterdDisplayDateWidget complies with IDisplayWidget.
+
+        >>> verifyClass(IDisplayWidget, FormattedDateDisplayWidget)
+        True
+    """
+
+    _WidgetFactory = FormattedDateDisplayWidget
+    expected_class = "date"
+
+    def setUp(self):
+        super(FormattedDateDisplayWidgetTest, self).setUp()
+        self._value = datetime.date(2004, 12, 01)
+
+    def testDefaultDisplayStyle(self):
+        self.assertFalse(self._widget.displayStyle)
+
+    def testRenderDefault(self):
+        self._widget.setRenderedValue(self._value)
+        self.verifyResult(self._widget(),
+                          ["<span",
+                           'class="%s"' % self.expected_class,
+                           "2004-12-01",
+                           "</span"])
+
+    def testRenderCustom(self):
+        self._widget.setRenderedValue(self._value)
+        self._widget.date_format = '%m/%d/%Y'
+        self.verifyResult(self._widget(),
+                          ["<span",
+                           'class="%s"' % self.expected_class,
+                           "12/01/2004",
+                           "</span"])
+
+    def testRenderNone(self):
+        self._widget.setRenderedValue(None)
+        self._widget.date_format = '%m/%d/%Y'
+        self.assertEqual(self._widget(), '')
+
+    def testNoValueSet(self):
+        self._widget.date_format = '%m/%d/%Y'
+        self.assertEqual(self._widget(), '')
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(FormattedDateWidgetTest),
+        unittest.makeSuite(FormattedDateDisplayWidgetTest),
+        doctest.DocTestSuite(),
+        ))
+
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_objectwidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_objectwidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_objectwidget.py	(revision 16045)
@@ -0,0 +1,234 @@
+## $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
+##
+"""Tests for objectwidget.
+"""
+import doctest
+import unittest
+import sys
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyClass
+from zope.publisher.browser import TestRequest
+from zope.schema import Object, TextLine
+from zope.schema.interfaces import ITextLine
+from zope.traversing.adapters import DefaultTraversable
+from zope.traversing.interfaces import ITraversable
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.component import provideAdapter
+
+from zope.formlib.interfaces import (
+    IInputWidget, IDisplayWidget,MissingInputError)
+from zope.formlib.widgets import TextWidget, DisplayWidget
+from zope.formlib.tests.test_browserwidget import BrowserWidgetTest
+from zope.formlib.interfaces import IWidgetInputErrorView
+
+from waeup.kofa.widgets.objectwidget import KofaObjectWidget as ObjectWidget
+from waeup.kofa.widgets.objectwidget import (
+    KofaObjectDisplayWidget as ObjectDisplayWidget)
+
+class ITestContact(Interface):
+    name = TextLine()
+    email = TextLine()
+
+class TestContact(object):
+    implements(ITestContact)
+
+class ITestContent(Interface):
+    foo = Object(
+        ITestContact,
+        title = u'Foo Title',
+        description = u'',
+        )
+
+class ObjectWidgetInputErrorView(object):
+    implements(IWidgetInputErrorView)
+
+    def __init__(self, error, request):
+        self.error = error
+        self.request = request
+
+    def snippet(self):
+        return repr(self.error)
+
+class ObjectWidgetTest(BrowserWidgetTest):
+    """Documents and tests the object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IInputWidget, ObjectWidget)
+        True
+    """
+
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+
+    def test_applyChanges(self):
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+
+        self.assertEqual(widget.applyChanges(self.content), True)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_error(self):
+        provideAdapter(
+            ObjectWidgetInputErrorView,
+            (MissingInputError, TestRequest),
+            IWidgetInputErrorView)
+
+        widget = self._WidgetFactory(self.field, self.request)
+        self.assertRaises(MissingInputError, widget.getInputValue)
+        error_html = widget.error()
+        if sys.version_info < (2, 5):
+            self.assertTrue("email: <zope.formlib.interfaces.Mis"
+                             in error_html)
+            self.assertTrue("name: <zope.formlib.interfaces.Miss"
+                             in error_html)
+        else:
+            self.assertTrue(
+                "email: MissingInputError(u'field.foo.email', u'', None)"
+                in error_html)
+            self.assertTrue(
+                "name: MissingInputError(u'field.foo.name', u'', None)"
+                in error_html)
+
+    def test_applyChangesNoChange(self):
+        self.content.foo = TestContact()
+        self.content.foo.name = u'Foo Name'
+        self.content.foo.email = u'foo@foo.test'
+
+        self.request.form['field.foo.name'] = u'Foo Name'
+        self.request.form['field.foo.email'] = u'foo@foo.test'
+        widget = self._WidgetFactory(self.field, self.request)
+        widget.setRenderedValue(self.content.foo)
+
+        self.assertEqual(widget.applyChanges(self.content), False)
+        self.assertEqual(hasattr(self.content, 'foo'), True)
+        self.assertEqual(isinstance(self.content.foo, TestContact), True)
+        self.assertEqual(self.content.foo.name, u'Foo Name')
+        self.assertEqual(self.content.foo.email, u'foo@foo.test')
+
+    def test_respect_custom_widgets(self):
+        # We can use our own subwidgets when creating ObjectWidgets
+        class CustomTextWidget(TextWidget):
+            pass
+
+        # We create a custom widget for the `name` field of ITestContact
+        name_widget = CustomTextWidget(ITestContact['name'], self.request)
+
+        # Custom widgets are passed by <fieldname>_widget keyword:
+        widget = self._WidgetFactory(self.field, self.request,
+                                     name_widget=name_widget)
+        widget1, widget2 = widget.subwidgets()
+        self.assertTrue(widget1 is name_widget)
+
+
+class ObjectDisplayWidgetTest(BrowserWidgetTest):
+    """Documents and tests the display variant of object widget.
+
+        >>> from zope.interface.verify import verifyClass
+        >>> verifyClass(IDisplayWidget, ObjectDisplayWidget)
+        True
+
+   """
+    _FieldFactory = Object
+    def _WidgetFactory(self, context, request, **kw):
+        kw.update({'factory': TestContact})
+        return ObjectDisplayWidget(context, request, **kw)
+
+    def setUpContent(self, desc=u'', title=u'Foo Title'):
+        provideAdapter(TextWidget, (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget, (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+        # The widget must be traversable. We register the default
+        # adapter that can turn nearly any object into an ITraversable.
+        provideAdapter(DefaultTraversable, (None,), ITraversable)
+
+        class TestObject(object):
+            implements(ITestContent)
+
+        self.content = TestObject()
+        self.content.name = u'Foo Name'
+        self.content.email = u'foo@foo.test'
+        self.field = ITestContent['foo']
+        self.request = TestRequest(HTTP_ACCEPT_LANGUAGE='pl')
+        self.request.form['field.foo'] = u'Foo Value'
+        self._widget = self._WidgetFactory(self.field, self.request)
+
+    def setUp(self):
+        super(ObjectDisplayWidgetTest, self).setUp()
+        self.field = Object(ITestContact, __name__=u'foo')
+        provideAdapter(TextWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IInputWidget)
+        provideAdapter(DisplayWidget,
+                       (ITextLine, IDefaultBrowserLayer),
+                       IDisplayWidget)
+
+
+    def test_interfaces(self):
+        self.assertTrue(IDisplayWidget.providedBy(self._widget))
+        self.assertFalse(IInputWidget.providedBy(self._widget))
+        self.assertTrue(verifyClass(IDisplayWidget, ObjectDisplayWidget))
+
+    def test_render(self):
+        widget = ObjectDisplayWidget(
+            self.field, self.request, TestContact)
+        widget.setRenderedValue(self.content)
+
+        check_list = [
+            '<span',
+            'Foo Name',
+            '</span>', '<span',
+            'foo@foo.test',
+            '</span>']
+        self.verifyResult(widget(), check_list, inorder=True)
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(ObjectWidgetTest),
+        unittest.makeSuite(ObjectDisplayWidgetTest),
+        doctest.DocTestSuite(),
+        ))
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_phonewidget.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_phonewidget.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/widgets/tests/test_phonewidget.py	(revision 16045)
@@ -0,0 +1,191 @@
+## $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
+##
+"""
+Tests for PhoneWidget.
+
+Most tests are reimplementation of similar tests in zope.formlib.
+"""
+from zope import schema
+from zope.component import getGlobalSiteManager
+from zope.formlib import form
+from zope.formlib.tests.test_functional_textwidget import(
+    FunctionalWidgetTestCase, patternExists)
+from zope.formlib.textwidgets import TextWidget
+from zope.interface import Interface, implements
+from zope.publisher.browser import TestRequest
+from zope.schema.interfaces import ITextLine
+from waeup.kofa.widgets.phonewidget import PhoneWidget
+
+# Dummy content
+class ISampleContent(Interface):
+    foo = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number',
+        required = True,
+        default=u'+234-1-1')
+
+    bar = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number (not required)',
+        required = False,
+        missing_value = u'')
+
+    baz = schema.TextLine(
+        title = u'Phone',
+        description = u'Required phone with a default',
+        required = False,
+        #default=u'+234--'
+        )
+
+class SampleContent:
+    implements(ISampleContent)
+
+    def __init__(self):
+        self.foo = None
+        self.bar = 'bar'
+        self.baz = None
+
+class SampleForm(form.EditForm):
+    form_fields = form.fields(ISampleContent)
+    form_fields['foo'].custom_widget = PhoneWidget
+    form_fields['bar'].custom_widget = PhoneWidget
+    form_fields['baz'].custom_widget = PhoneWidget
+
+class PhoneWidgetTests(FunctionalWidgetTestCase):
+
+    widgets = [
+        (ITextLine, TextWidget),
+        (ITextLine, TextWidget),
+        (ITextLine, TextWidget),
+        ]
+
+    def test_display_editform(self):
+        content = SampleContent()
+        request = TestRequest()
+        html = SampleForm(content, request)()
+        # foo.country, foo.area and foo.ext exist
+        self.assert_(patternExists(
+            '<select .* name="form.foo.country".*>', html))
+        self.assert_(patternExists(
+            '<input .* name="form.foo.area".* value="".*>', html))
+        self.assert_(patternExists(
+            '<input .* name="form.foo.ext".* value="".*>', html))
+        return
+
+    def test_submit_editform(self):
+        # we can submit an edit form
+        content = SampleContent()
+        request = TestRequest()
+
+        # submit edit view
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u'7890'
+        request.form['form.actions.apply'] = u''
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.foo, u'+123-456-7890')
+        return
+
+    def test_invalid_type(self):
+        # there is no invalid type for textline-based input
+        content = SampleContent()
+        request = TestRequest()
+
+        # submit invalid type for text line
+        request.form['form.foo.country'] = '+123'
+        request.form['form.foo.area'] = '456'
+        request.form['form.foo.ext'] = '7890'
+        request.form['form.actions.apply'] = u''
+        html = SampleForm(content, request)()
+
+        # We don't have a invalid field value
+        # since we convert the value to unicode
+        self.assert_('Object is of wrong type.' not in html)
+        return
+
+    def test_missing_value(self):
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u'7890'
+        request.form['form.bar.country'] = u''
+        request.form['form.bar.area'] = u''
+        request.form['form.bar.ext'] = u''
+        request.form['form.baz.country'] = u''
+        request.form['form.baz.area'] = u''
+        request.form['form.baz.ext'] = u''
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.foo, u'+123-456-7890')
+        self.assertEqual(content.bar, u'') # default missing value
+        self.assertEqual(content.baz, None)
+        return
+
+    def test_partial_values(self):
+        # make sure partial numbers will not be enough
+        # XXX: we have to test each single field alone as an error
+        #      in one field will stop setting the other ones.
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.foo.country'] = u'+123'
+        request.form['form.foo.area'] = u'456'
+        request.form['form.foo.ext'] = u''
+        request.form['form.bar.country'] = u'+123'
+        request.form['form.bar.area'] = u'12'
+        request.form['form.bar.ext'] = u'789'
+        request.form['form.baz.country'] = u'+123'
+        request.form['form.baz.area'] = u'456'
+        request.form['form.baz.ext'] = u'789'
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        # as there were errors in the form, no value was set at all
+        self.assertEqual(content.foo, None)
+        self.assertEqual(content.bar, 'bar')
+        self.assertEqual(content.baz, None)
+        return
+
+    def test_no_values(self):
+        # if the last two subfields contain no value, no phone will be set
+        content = SampleContent()
+        request = TestRequest()
+
+        request.form['form.bar.country'] = u'+123'
+        request.form['form.bar.area'] = u''
+        request.form['form.bar.ext'] = u''
+        request.form['form.baz.country'] = u'+124'
+        request.form['form.baz.area'] = u''
+        request.form['form.baz.ext'] = u''
+        request.form['form.actions.apply'] = u''
+
+        SampleForm(content, request)()
+
+        # check new values in object
+        self.assertEqual(content.bar, u'') # default missing value
+        self.assertEqual(content.baz, None)
+        return
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/workflow.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/workflow.py	(revision 16045)
@@ -0,0 +1,92 @@
+## $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
+##
+"""Workflow components of general use.
+"""
+import grok
+from hurry.workflow.interfaces import IWorkflow, MANUAL, InvalidTransitionError
+from hurry.workflow.workflow import (
+    Transition, Workflow, WorkflowVersions, WorkflowInfo,
+    NullCondition, nullCheckPermission)
+from zope.security.interfaces import NoInteraction
+from zope.security.management import getInteraction
+from waeup.kofa.interfaces import IKofaWorkflowInfo
+
+class KofaWorkflow(Workflow):
+    """A :mod:`hurry.workflow` workflow with more appropriate error
+       messages.
+    """
+    grok.provides(IWorkflow)
+
+    def getTransition(self, source, transition_id):
+        transition = self._id_transitions[transition_id]
+        if transition.source != source:
+            raise InvalidTransitionError(
+                "Transition '%s' requires '%s' as source state (is: '%s')" % (
+                    transition_id, transition.source, source))
+        return transition
+
+
+class WorkflowNullVersions(WorkflowVersions):
+    """A workflow versions manager that does not handle versions.
+
+    Sounds odd, but the default implementation of
+    :class:`hurry.workflow.workflow.WorkflowVersions` is a base
+    implementation that raises :exc:`NotImplemented` exceptions for
+    most of the methods defined below.
+
+    If we want to register a versionless workflow, an utility
+    implementing IWorkflowVersions is looked up nevertheless by
+    WorkflowInfo and WorkflowState components so we **have** to
+    provide workflow versions, even if we do not support versioned
+    workflows.
+
+    This implementation returns empty result sets for any requests,
+    but does not raise :exc:`NotImplemented`.
+    """
+    def getVersions(self, state, id):
+        return []
+
+    def getVersionsWithAutomaticTransitions(self):
+        return []
+
+    def hasVersion(self, id, state):
+        return False
+
+    def hasVersionId(self, id):
+        return False
+
+class KofaWorkflowInfo(WorkflowInfo):
+    """A workflow info that provides a convenience transition getter.
+    """
+
+    grok.provides(IKofaWorkflowInfo)
+
+    def getManualTransitions(self):
+        """Get allowed manual transitions.
+
+        Get a sorted list of tuples containing the `transition_id` and
+        `title` of each allowed transition.
+        """
+        try:
+            checkPermission = getInteraction().checkPermission
+        except NoInteraction:
+            checkPermission = nullCheckPermission
+        return [(transition.transition_id, transition.title)
+                for transition in sorted(self._getTransitions(MANUAL))
+                if transition.condition(self, self.context) and
+                checkPermission(transition.permission, self.context)]
Index: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/zcml.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/src/waeup/kofa/zcml.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/src/waeup/kofa/zcml.py	(revision 16045)
@@ -0,0 +1,50 @@
+## $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
+##
+from zope.component.zcml import handler
+from waeup.kofa.interfaces import IDataCenterConfig
+
+def data_center_conf(context, path):
+    """Handler for ZCML ``datacenter`` directive.
+
+    Registers a global utility under IDataCenterConfig and containing
+    a dictionary with (currently) only one entry: `path`.
+
+    The directive can be put into site.zcml like this:
+
+    - Add to the header:
+        ``xmlns:kofa="http://namespaces.waeup.org/kofa"``
+
+    - Then, after including waeup.kofa:
+        ``<kofa:datacenter path="some/existing/file/path" />``
+
+    In a running instance (where some directive like above was
+    processed during startup), one can then ask for the
+    IDataCenterConfig utility:
+
+      >>> from waeup.kofa.interfaces import IDataCenterConfig
+      >>> from zope.component import getUtility
+      >>> getUtility(IDataCenterConfig)
+      {'path': 'some/existing/file/path'}
+
+    """
+    context.action(
+        discriminator = ('utility', IDataCenterConfig, ''),
+        callable = handler,
+        args = ('registerUtility',
+                {'path':path}, IDataCenterConfig, '')
+        )
Index: main/waeup.kofa/branches/uli-py3/tools/add_old_id_col.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/add_old_id_col.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/add_old_id_col.py	(revision 16045)
@@ -0,0 +1,31 @@
+"""Add a new column old id with contents from col `student_id`
+"""
+
+###
+### CONFIGURATION
+###
+
+IN_FILE = 'mytry.csv'
+
+# new col name. 'old_id' is expected by fix_import_file.py
+NEW_COL = 'old_id'
+
+###
+### CONFIGURATION - END
+###
+
+
+import csv
+OUT_FILE = '%s_edited.csv' % IN_FILE.split('.')[0]
+reader = csv.DictReader(open(IN_FILE, 'rb'))
+
+for num, row in enumerate(reader):
+    if num == 0:
+        writer = csv.DictWriter(
+            open(OUT_FILE, 'wb'), reader.fieldnames + [NEW_COL,])
+        header = dict([(x, x) for x in writer.fieldnames])
+        writer.writerow(header)
+    row[NEW_COL] = row['student_id']
+    writer.writerow(row)
+
+print "RESULT in ", OUT_FILE
Index: main/waeup.kofa/branches/uli-py3/tools/copy_media_files.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/copy_media_files.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/copy_media_files.py	(revision 16045)
@@ -0,0 +1,140 @@
+##
+## This script is called without parameters
+##
+## It copies media files from an old SRP folder over to a Kofa
+## instance media folder. It does not remove copied folders.
+##
+## Configuration is done below.
+##
+## At end a list of dirs that were copied or are empty is returned.
+##
+## Please note that for changing groups of files/directories, you
+## normally have to be root. This script is therefore normally run with
+## sudo.
+##
+
+## ######################################################################
+## CONFIGURATION
+##
+
+## The folder where all source docs can be found
+#SRC_DIR = "/zope/instances/aaue-images"
+SRC_DIR = "/tmp/migration/src"
+
+## The students folder in Kofa where files should go to
+#DST_DIR = "/kofa/aaue/var/datacenter/media/students"
+DST_DIR = "/tmp/migration/dst"
+
+## The Ids of new students to search for in old portal.
+NEW_IDS_CSV = "StudentIds.csv"
+
+## The ids of students that will will get a complete new number, not
+## an updated one. Must contain a mapping of old ids to new ids.
+## If no such update should be performed, please set the constant to None.
+ID_MAP_CSV = None
+ID_MAP_CSV = "id_mapping.csv"
+
+## Permissions to be set on new files/dirs. Set OWNER and/or GROUP to
+## None to leave them unchanged after creation.
+UMASK = 0664
+OWNER = 'uli'
+GROUP = 'nogroup'
+
+##
+## CONFIGURATION END
+## ######################################################################
+
+import csv
+import os
+import shutil
+import sys
+from grp import getgrnam
+from pwd import getpwnam
+
+OWNER_ID = OWNER and getpwnam(OWNER)[2] or -1
+GRP_ID = GROUP and getgrnam(GROUP)[2] or -1
+DIR_UMASK = UMASK | 0111  # directories need extra x-permission
+
+def set_perms(path):
+    # set permissions and ownership for path
+    if os.path.isdir(path):
+        os.chmod(path, DIR_UMASK)
+    else:
+        os.chmod(path, UMASK)
+    os.chown(path, OWNER_ID, GRP_ID)
+    return
+
+def new_folder_name(stud_id):
+    # compute new folder name from stud_id (old stud_ids only)
+    num = int(stud_id[1:])
+    num = num / 10000 * 10
+    return '%05d' % num
+
+def create_path(path):
+    # create path with subdirs, if it does not exist (completely)
+    if os.path.exists(path):
+        return
+    parent = os.path.dirname(path)
+    if not os.path.exists(parent):
+        # create parent first
+        create_path(parent)
+    print "CREATE PATH ", path
+    os.mkdir(path)
+    set_perms(path)
+    return
+
+def copy_file(file_src, file_dst):
+    create_path(os.path.dirname(file_dst))
+    print "COPY FILE: %s -> %s" % (file_src, file_dst)
+    shutil.copyfile(file_src, file_dst)
+    set_perms(file_dst)
+    return
+
+def get_new_old_id_mapping():
+    """Returns a dict mapping from _new_ ids to old (SRP) ids.
+
+    The dict is read from ID_MAP_CSV file. If this var is set to
+    ``None`` an empty dict is returned. The ID_MAP_CSV contains only
+    the student ids of those students, for which the standard method
+    (new_id=CHAR+old_id) does not work.
+    """
+    if ID_MAP_CSV is None:
+        return {}
+    if not os.path.isfile(ID_MAP_CSV):
+        raise IOError(
+            "No such file for mapping new to old ids: %s" % ID_MAP_CSV)
+    result = dict()
+    reader = csv.DictReader(open(ID_MAP_CSV, 'rb'))
+    for row in reader:
+        result[row['new_id']] = row['student_id']
+    return result
+
+# special ids not handled in common way
+new_to_old_map = get_new_old_id_mapping()
+removable_dirs = []
+reader = csv.DictReader(open(NEW_IDS_CSV, 'rb'))
+for row in reader:
+    new_stud_id = row['student_id']
+    stud_id = new_to_old_map.get(new_stud_id[1:], new_stud_id[1:])
+    src_folder = os.path.join(SRC_DIR, stud_id[0], stud_id)
+    dst_folder = os.path.join(
+        DST_DIR, new_folder_name(new_stud_id[1:]), new_stud_id)
+    if not os.path.exists(src_folder):
+        print "No old data for %s. Skipping." % stud_id
+        continue
+    if os.path.exists(dst_folder):
+        print "Destination folder exists already: %s. Skipping" % (dst_folder)
+        continue
+    removable_dirs.append(src_folder)
+    src_files = os.listdir(src_folder)
+    if len(src_files) == 0:
+        print "Empty source folder for %s. Skipping." % stud_id
+        continue
+    for name in src_files:
+        file_src = os.path.join(src_folder, name)
+        file_dst = os.path.join(dst_folder, name.replace(stud_id, new_stud_id))
+        copy_file(file_src, file_dst)
+
+print "DIRS TO REMOVE: "
+for name in removable_dirs:
+    print name
Index: main/waeup.kofa/branches/uli-py3/tools/download_exports.sh
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/download_exports.sh	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/download_exports.sh	(revision 16045)
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+## We expect at least three arguments...
+if [ $# -ne 3 ]; then
+    echo "Usage: $0 [user name] [password] [exporter]"
+    exit
+fi
+
+wget --save-cookies cookies.txt --keep-session-cookies --post-data "form.login=$1&form.password=$2" https://uniben-kofa.waeup.org/login 
+wget --load-cookies cookies.txt  --output-document $3.csv https://uniben-kofa.waeup.org/datacenter/export.csv?exporter=$3
+tar -czf Kofa$3.tar.gz $3.csv
+rm $3.csv
+mv Kofa$3.tar.gz /home/ftp/uniben
Index: main/waeup.kofa/branches/uli-py3/tools/fix_import_file.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/fix_import_file.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/fix_import_file.py	(revision 16045)
@@ -0,0 +1,569 @@
+## $Id$
+## 
+## Copyright (C) 2012 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
+##
+"""
+Fix exports from old SRP portal and other data sources to make
+them importable by current portal.
+
+Usage:
+
+Change into this directory, set the options below (files are assumed
+to be in the same directory) and then run
+
+  python fix_import_file.py <filename>
+
+Errors/warnings will be displayed on the shell, the output will be put
+into the specified output file.
+
+
+The lgas.py module must be copied into the same folder where this script
+is started.
+"""
+import csv
+import datetime
+import os
+import re
+import sys
+
+try:
+    from lgas import LGAS
+except:
+    print 'ERROR: lgas.py is missing.'
+    sys.exit(1)
+
+def strip(string):
+    string = string.replace('_', '')
+    string = string.replace('/', '')
+    string = string.replace('-', '')
+    string = string.replace(' ', '')
+    string = string.lower()
+    return string
+
+LGAS_inverted_stripped = dict([(strip(i[1]), i[0]) for i in LGAS])
+LGAS_dict = dict(LGAS)
+
+##
+## CONFIGURATION SECTION
+##
+# keys are fieldnames in input file, values are methods of class
+# Converter (see below)
+OPTIONS = {
+    'student_id': 'student_id',
+    'sex': 'gender',
+    'birthday': 'date',
+    'marit_stat': 'marit_stat',
+    'session': 'session',
+    'entry_session': 'session',
+    'current_session': 'session',
+    'session_id': 'session',
+    'entry_mode': 'mode',
+    'reg_state': 'reg_state',
+    'password': 'password',
+    'phone': 'phone',
+    'nationality': 'nationality',
+    'level': 'level',
+    'start_level': 'level',
+    'end_level': 'level',
+    'level_id': 'level',
+    'current_level': 'level',
+    'semester': 'semester',
+    'application_category': 'application_category',
+    'lga': 'lga',
+    'order_id': 'no_int',
+    'uniben': 'former',
+    'nysc_year': 'year',
+    'alr_date': 'date',
+    'fst_sit_date': 'date',
+    'scd_sit_date': 'date',
+    'emp_start': 'date',
+    'emp_end': 'date',
+    'emp_start2': 'date',
+    'emp_end2': 'date',
+    'fst_sit_results': 'result',
+    'scd_sit_results': 'result',
+    'alr_results': 'result',
+    'email': 'email',
+    'fst_sit_type': 'sittype',
+    'scd_sit_type': 'sittype',
+    'resp_pay_reference': 'no_int',
+    'type': 'company',
+    'date': 'date',
+    'core_or_elective': 'bool',
+    'category': 'p_category',
+    'reg_transition': 'reg_state',  # we completely change this column,
+                                    # since reg_state import is usually intended
+    'transition': 'reg_transition',
+    'payment_date': 'date',
+    'validation_date': 'date',
+    'resp_approved_amount': 'amount',
+    'amount': 'amount',
+    'amount_auth': 'amount',
+    'surcharge': 'amount'
+    }
+
+# Mapping input file colnames --> output file colnames
+COLNAME_MAPPING = {
+    # base data
+    'id': 'student_id',
+    'reg_state': 'state',
+    'reg_transition': 'state',
+    'jamb_reg_no': 'reg_number',
+    'matric_no': 'matric_number',
+    'birthday': 'date_of_birth',
+    'clr_ac_pin': 'clr_code',
+    # clearance
+    'hq_grade': 'hq_degree',
+    'uniben': 'former_matric',
+    'hq_type2': 'hq2_type',
+    'hq_grade2': 'hq2_degree',
+    'hq_school2': 'hq2_school',
+    'hq_matric_no2': 'hq2_matric_no',
+    'hq_session2': 'hq2_session',
+    'hq_disc2': 'hq2_disc',
+    'emp': 'employer',
+    'emp2': 'employer2',
+    'emp_position2': 'emp2_position',
+    'emp_start2': 'emp2_start',
+    'emp_end2': 'emp2_end',
+    'emp_reason2': 'emp2_reason',
+    # study course
+    'study_course': 'certificate',
+    # study level
+    'session': 'level_session',
+    'verdict': 'level_verdict',
+    # course ticket
+    'level_id': 'level',
+    'core_or_elective': 'mandatory',
+    # payment ticket
+    'order_id': 'p_id',
+    'status': 'p_state',
+    'category': 'p_category',
+    'resp_pay_reference': 'r_pay_reference',
+    'resp_desc': 'r_desc',
+    'resp_approved_amount': 'r_amount_approved',
+    'item': 'p_item',
+    'amount': 'amount_auth',
+    'resp_card_num': 'r_card_num',
+    'resp_code': 'r_code',
+    'date': 'creation_date',
+    'surcharge': 'surcharge_1',
+    'session_id': 'p_session',
+    'type': 'r_company',
+    'old_id': 'old_id',
+    }
+
+# Mapping input state --> output state
+REGSTATE_MAPPING = {
+    'student_created': 'created',
+    'admitted': 'admitted',
+    'objection_raised': 'clearance started',
+    'clearance_pin_entered': 'clearance started',
+    'clearance_requested': 'clearance requested',
+    'cleared_and_validated': 'cleared',
+    'school_fee_paid': 'school fee paid',
+    'returning': 'returning',
+    'courses_registered': 'courses registered',
+    'courses_validated': 'courses validated',
+    'admit': 'admitted',
+    'return': 'returning'
+    }
+
+# Mapping of special cases, where new id is not deductible from old id
+# Set to `None`, if no such special cases should be considered.
+ID_MAP_CSV = None
+ID_MAP_CSV = "id_mapping.csv"
+
+##
+## END OF CONFIG
+##
+
+# Look for the first sequence of numbers
+RE_PHONE = re.compile('[^\d]*(\d*)[^\d]*')
+
+def get_id_mapping():
+    """Returns a dict mapping from old (SRP) ids to new ids.
+
+    The dict is read from ID_MAP_CSV file. If this var is set to
+    ``None`` an empty dict is returned. The ID_MAP_CSV contains only
+    the student ids of those students, for which the standard method
+    (new_id=CHAR+old_id) does not work.
+    """
+    if ID_MAP_CSV is None:
+        return {}
+    if not os.path.isfile(ID_MAP_CSV):
+        raise IOError(
+            "No such file for mapping old to new ids: %s" % ID_MAP_CSV)
+    result = dict()
+    reader = csv.DictReader(open(ID_MAP_CSV, 'rb'))
+    for row in reader:
+        result[row['student_id']] = row['new_id']
+    return result
+
+
+def convert_fieldnames(fieldnames):
+    """Replace input fieldnames by fieldnames of COLNAME_MAPPING.
+    """
+    # Remove whitespaces
+    header = dict([(name, name.strip()) for name in fieldnames])
+    for in_name, out_name in COLNAME_MAPPING.items():
+        if in_name not in header.values():
+            continue
+        # Inverse dictionary lookup
+        key = [key for key,value in header.items() if value==in_name][0]
+        header[key] = out_name
+    return header
+
+class Converters():
+    """Converters to turn old-style values into new ones.
+    """
+
+    old_new_id_map = get_id_mapping()
+
+    @classmethod
+    def student_id(cls, value, row):
+        """ 'A123456' --> 'EA123456'
+        """
+        value = cls.old_new_id_map.get(value, value)
+        if len(value) == 7:
+            return 'B' + value
+        return value
+
+    @classmethod
+    def reg_state(self, value, row):
+        """ 'courses_validated' --> 'courses validated'
+        """
+        return REGSTATE_MAPPING.get(value,value)
+
+    @classmethod
+    def reg_transition(self, value, row):
+        if value == "admitted":
+            return "admit"
+        if value == "returning":
+            return "return"
+        return value
+
+    @classmethod
+    def level(self, value, row):
+        """ '000' --> '10'
+        '800' --> '999' if pg student
+        """
+        try:
+            number = int(value)
+        except ValueError:
+            return 9999
+        if number == 0:
+            return 10
+        if row.get('entry_mode') and row.get('entry_mode').startswith('pg'):
+            return 999
+        return number
+
+    @classmethod
+    def semester(self, value, row):
+        """ '0' --> '9'
+        """
+        try:
+            number = int(value)
+        except ValueError:
+            return 9999
+        if number == 0:
+            return 9
+        return number
+
+    @classmethod
+    def application_category(self, value, row):
+        """ '' --> 'no'
+        """
+        if value == '':
+            return 'no'
+        return value
+
+    @classmethod
+    def lga(self, value, row):
+        """ Remove apostrophe
+        """
+        if value == 'akwa_ibom_uru_offong_oruko':
+            return 'akwa_ibom_urue-offong-oruko'
+        if value == 'edo_ohionmwon':
+            return 'edo_orhionmwon'
+
+        if value == 'nassarawa_nassarawa':
+            return 'nassarawa_nassawara'
+
+        if value == 'kogi_mopa-muro-mopi':
+            return 'kogi_mopa-muro'
+
+        if value == 'delta_osimili-north':
+            return 'delta_oshielli-north'
+
+        if value == 'delta_osimili':
+            return 'delta_oshimili'
+
+        if value == 'delta_osimili-south':
+            return 'delta_oshimili-south'
+        try:
+            value = value.replace("'","")
+        except:
+            return ''
+        lower = value.lower()
+        if lower in LGAS_dict.keys():
+            return lower
+        # If real names are given, let's see if a similar value
+        # in LGAS exist.
+        value = value.replace('_',' ')
+        value = LGAS_inverted_stripped.get(strip(lower), value)
+        return value
+
+
+    @classmethod
+    def session(self, value, row):
+        """ '08' --> '2008'
+        '2008/2009' --> '2008'
+        """
+        if '/' in value:
+            numbers = value.split('/')
+            number = int(numbers[0])
+            if number in range(2000,2015):
+                return number
+            else:
+                return 9999
+        try:
+            number = int(value)
+        except ValueError:
+            return 9999
+        if number < 14:
+            return number + 2000
+        elif number in range(2000,2015):
+            return number
+        else:
+            return 9999
+
+    @classmethod
+    def former(self, value, row):
+        """ True --> yes
+        '2008/2009' --> '2008'
+        """
+        if value == 'True':
+            return 'yes'
+        return
+
+    @classmethod
+    def bool(self, value, row):
+        """ True --> 1
+        """
+        if value in ('TRUE', 'True'):
+            return '1'
+        elif value in ('FALSE', 'False'):
+            return '0'
+        return
+
+    @classmethod
+    def year(self, value, row):
+        """ '0' --> ''
+        """
+        if value == '0':
+            return
+        if value == 'None':
+            return
+        return value
+
+
+    @classmethod
+    def marit_stat(self, value, row):
+        """ 'True'/'False' --> 'married'/'unmarried'
+        """
+        if value in ('True','married'):
+            value = 'married'
+        elif value in ('False','unmarried'):
+            value = 'unmarried'
+        else:
+            value = ''
+        return value
+
+    @classmethod
+    def gender(self, value, row):
+        """ 'True'/'False' --> 'f'/'m'
+        """
+        if value.strip() in ('F', 'True','f'):
+            value = 'f'
+        elif value.strip() in ('M', 'False','m'):
+            value = 'm'
+        else:
+            value = ''
+        return value
+
+    @classmethod
+    def date(self, value, row):
+        """ 'yyyy/mm/dd' --> 'yyyy-mm-dd'
+        """
+        if value == "None":
+            value = ""
+        elif value == "":
+            value = ""
+        else:
+            value = value.replace('/', '-')
+            # We add the hash symbol to avoid automatic date transformation
+            # in Excel and Calc for further processing
+            value += '#'
+        return value
+
+    @classmethod
+    def no_int(self, value, row):
+        """ Add hash and skip numbers starting with 999999
+        """
+        # We add the hash symbol to avoid automatic number transformation
+        # in Excel and Calc for further processing
+        try:
+            intvalue = int(value)
+            value += '#'
+        except:
+            pass
+        if value.startswith('999999'):
+            return
+        return value
+
+    @classmethod
+    def amount(self, value, row):
+        """ Amounts must be integers
+        """
+        try:
+            return float(value)
+        except:
+            return
+
+    @classmethod
+    def mode(self, value, row):
+        if value == "transfer_fulltime":
+            return "transfer_ft"
+        if value == "ume_ft":
+            return "utme_ft"
+        return value
+
+    @classmethod
+    def password(self, value, row):
+        if value == "not set":
+            return ""
+        return value
+
+    @classmethod
+    def nationality(self, value, row):
+        if value in ('nigeria', 'Nigeria'):
+            return "NG"
+        if value in ('niger', 'Niger'):
+            return "NE"
+        return value
+
+    @classmethod
+    def sittype(self, value, row):
+        if value == "nabtec":
+            return "nabteb"
+        return value
+
+    @classmethod
+    def company(self, value, row):
+        if value == "online":
+            return "interswitch"
+        return value
+
+    @classmethod
+    def p_category(self, value, row):
+        if value == "acceptance":
+            return "clearance"
+        return value
+
+    @classmethod
+    def email(self, value, row):
+        return value.strip()
+
+    @classmethod
+    def phone(self, value, row):
+        """ '<num-seq1>-<num-seq2> asd' -> '--<num-seq1><num-seq2>'
+
+        Dashes and slashes are removed before looking for sequences
+        of numbers.
+        """
+        if not value:
+            return
+        if len(value) < 5:
+            return
+        value = value.strip('#')
+        value = value.replace('-', '')
+        value = value.replace('/', '')
+        match = RE_PHONE.match(value)
+        phone = match.groups()[0]
+        if value.startswith('234'):
+            value = '+' + value[:3] + '-' + value[3:]
+        elif value.startswith('+234'):
+            value = value[:4] + '-' + value[4:]
+        else:
+            value = '-%s' % phone
+        return value + '#'
+
+    @classmethod
+    def result(self, value, row):
+        try:
+            liste = eval(value)
+        except:
+            return
+        if isinstance(liste,list):
+            return [(i[0].lower(),i[1]) for i in liste]
+        return
+
+
+def main():
+    input_file = '%s' % sys.argv[1]
+    output_file = '%s_edited.csv' % sys.argv[1].split('.')[0]
+    reader = csv.DictReader(open(input_file, 'rb'))
+    writer = None
+
+    for num, row in enumerate(reader):
+        if num == 0:
+            writer = csv.DictWriter(open(output_file, 'wb'), reader.fieldnames)
+            print "FIELDS: "
+            for x, y in enumerate(reader.fieldnames):
+                print x, y
+            header = convert_fieldnames(reader.fieldnames)
+            writer.writerow(header)
+        #if row.get('reg_state') == 'student_created':
+        #    # We do not reimport student records which have never been accessed.
+        #    continue
+        if row.get('status') == 'started':
+            # We do not reimport started payments.
+            continue
+        for key, value in row.items():
+            # Remove unwanted whitespaces.
+            row[key] = row[key].strip()
+            if not key in OPTIONS.keys():
+                continue
+            conv_name = OPTIONS[key]
+            converter = getattr(Converters, conv_name, None)
+            if converter is None:
+                print "WARNING: cannot find converter %s" % conv_name
+                continue
+            row[key] = converter(row[key], row)
+        try:
+            writer.writerow(row)
+        except:
+            print row['student_id']
+
+    print "Output written to %s" % output_file
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 2:
+        print 'Usage: %s <filename>' % __file__
+        sys.exit(1)
+    main()
Index: main/waeup.kofa/branches/uli-py3/tools/make_ssha.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/make_ssha.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/make_ssha.py	(revision 16045)
@@ -0,0 +1,28 @@
+import hashlib
+import os
+import sys
+from base64 import urlsafe_b64encode as encode
+from base64 import urlsafe_b64decode as decode
+
+def makeSecret(password):
+    salt = os.urandom(4)
+    h = hashlib.sha1(password)
+    h.update(salt)
+    return "{SSHA}" + encode(h.digest() + salt)
+
+def checkPassword(password, challenge_password):
+    challenge_bytes = decode(challenge_password[6:])
+    digest = challenge_bytes[:20]
+    salt = challenge_bytes[20:]
+    hr = hashlib.sha1(password)
+    hr.update(salt)
+    return digest == hr.digest()
+
+if __name__=='__main__':
+    if len(sys.argv) < 3:
+        print 'Usage: %s <encode or check> <password> <ssha string>' % __file__
+        sys.exit(1)
+    if sys.argv[1] == 'encode':
+        print makeSecret('%s' % sys.argv[2])
+    if sys.argv[1] == 'check':
+        print checkPassword('%s' % sys.argv[2], '%s' % sys.argv[3])
Index: main/waeup.kofa/branches/uli-py3/tools/prepare_jamb_file.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/prepare_jamb_file.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/prepare_jamb_file.py	(revision 16045)
@@ -0,0 +1,197 @@
+## $Id$
+## 
+## Copyright (C) 2012 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
+##
+"""
+"""
+import csv
+import datetime
+import re
+import sys
+
+##
+## CONFIGURATION SECTION
+##
+# keys are fieldnames in input file, values are methods of class
+# Converter (see below)
+OPTIONS = {
+    'sex': 'gender',
+    'session': 'session',
+    'lga': 'lga',
+    'firstname': 'name',
+    'middlename': 'name',
+    'lastname': 'uppername',
+    'container_code': 'container_code',
+    }
+
+# Mapping input file colnames --> output file colnames
+COLNAME_MAPPING = {
+    'jambscore': 'jamb_score',
+    'eng_score': 'jamb_subjects',
+    }
+
+##
+## END OF CONFIG
+##
+
+# Look for the first sequence of numbers
+RE_PHONE = re.compile('[^\d]*(\d*)[^\d]*')
+
+def convert_fieldnames(fieldnames):
+    """Replace input fieldnames by fieldnames of COLNAME_MAPPING.
+    """
+    header = dict([(name, name) for name in fieldnames])
+    for in_name, out_name in COLNAME_MAPPING.items():
+        if in_name not in header:
+            continue
+        header[in_name] = out_name
+    return header
+
+def merge_subjects(row):
+    """Merge jamb subjects and scores into one field.
+    """
+    if 'eng_score' in row and 'Subj2'	in row and 'Subj2Score' in row \
+        and 'Subj3' in row and 'Subj3Score' in row and 'Subj4' in row  \
+        and 'Subj4Score' in row:
+        subjectstring = "English: %s, %s: %s, %s: %s, %s: %s"
+        row['eng_score'] =  subjectstring % (
+            row['eng_score'],
+            row['Subj2'], row['Subj2Score'],
+            row['Subj3'], row['Subj3Score'],
+            row['Subj4'], row['Subj4Score'],
+            )
+    else:
+        pass
+
+class Converters():
+    """Converters to turn old-style values into new ones.
+    """
+
+    @classmethod
+    def name(self, value):
+        """ 'JOHN -> John'
+        """
+        return value.capitalize()
+
+    @classmethod
+    def uppername(self, value):
+        """ 'John -> JOHN'
+        """
+        return value.upper()
+
+    @classmethod
+    def container_code(self, value):
+        """ Return constant string.
+        """
+        return 'putme2012'
+
+    @classmethod
+    def lga(self, value):
+        """ 
+        """
+        if value == 'akwa_ibom_uru_offong_oruko':
+            return 'akwa_ibom_urue-offong-oruko'
+        if value == 'edo_akoko_edo':
+            return 'edo_akoko-edo'
+        if value == 'edo_owan_east':
+            return 'edo_owan-east'
+        if value == 'kogi_mopa-muro-mopi':
+            return 'kogi_mopa-muro'
+        if value == 'foreign':
+            return 'foreigner'
+        try:
+            value = value.replace("'","")
+            value = value.lower()
+        except:
+            return ''
+        return value
+
+
+    @classmethod
+    def session(self, value):
+        """ '08' --> '2008'
+        '2008/2009' --> '2008'
+        """
+        if '/' in value:
+            numbers = value.split('/')
+            number = int(numbers[0])
+            if number in range(2000,2015):
+                return number
+            else:
+                return 9999
+        try:
+            number = int(value)
+        except ValueError:
+            #import pdb; pdb.set_trace()
+            return 9999
+        if number < 14:
+            return number + 2000
+        elif number in range(2000,2015):
+            return number
+        else:
+            return 9999
+
+    @classmethod
+    def gender(self, value):
+        """ 'True'/'False' --> 'f'/'m'
+            'F'/'M' --> 'f'/'m'
+        """
+        if value in ('True','f','F'):
+            value = 'f'
+        elif value in ('False','m','M'):
+            value = 'm'
+        else:
+            value = ''
+        return value
+
+
+def main():
+    input_file = '%s' % sys.argv[1]
+    output_file = '%s_edited.csv' % sys.argv[1].split('.')[0]
+    reader = csv.DictReader(open(input_file, 'rb'))
+    writer = None
+
+    for num, row in enumerate(reader):
+        if num == 0:
+            writer = csv.DictWriter(open(output_file, 'wb'), reader.fieldnames)
+            print "FIELDS: "
+            for x, y in enumerate(reader.fieldnames):
+                print x, y
+            header = convert_fieldnames(reader.fieldnames)
+            writer.writerow(header)
+        merge_subjects(row)
+        for key, value in row.items():
+            if not key in OPTIONS.keys():
+                continue
+            conv_name = OPTIONS[key]
+            converter = getattr(Converters, conv_name, None)
+            if converter is None:
+                print "WARNING: cannot find converter %s" % conv_name
+                continue
+            row[key] = converter(row[key])
+        try:
+            writer.writerow(row)
+        except:
+            print row['reg_number']
+
+    print "Output written to %s" % output_file
+
+
+if __name__ == '__main__':
+    if len(sys.argv) != 2:
+        print 'Usage: %s <filename>' % __file__
+        sys.exit(1)
+    main()
Index: main/waeup.kofa/branches/uli-py3/tools/reload_fpm.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/reload_fpm.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/reload_fpm.py	(revision 16045)
@@ -0,0 +1,104 @@
+##
+## This script is called without parameters.
+##
+## It moves fingerprint minutiae from a reload folder to their right place in
+## the media folder.
+##
+## Configuration is done below.
+##
+##
+## Please note that for changing groups of files/directories, you
+## normally have to be root. This script is therefore normally run with
+## sudo.
+##
+
+## ######################################################################
+## CONFIGURATION
+##
+
+## The folder where all source docs can be found (can be any folder)
+#SRC_DIR = "/kofa/uniben/var/datacenter/media/students/reload"
+SRC_DIR = "/kofa/uniben_fpm"
+
+## The students folder in Kofa where files should go to
+DST_DIR = "/kofa/uniben/var/datacenter/media/students"
+
+
+## Permissions to be set on new files/dirs. Set OWNER and/or GROUP to
+## None to leave them unchanged after creation.
+UMASK = 0664
+OWNER = 'kofa'
+GROUP = 'kofa'
+
+##
+## CONFIGURATION END
+## ######################################################################
+
+import csv
+import os
+import shutil
+import sys
+from grp import getgrnam
+from pwd import getpwnam
+
+OWNER_ID = OWNER and getpwnam(OWNER)[2] or -1
+GRP_ID = GROUP and getgrnam(GROUP)[2] or -1
+DIR_UMASK = UMASK | 0111  # directories need extra x-permission
+
+def set_perms(path):
+    # set permissions and ownership for path
+    if os.path.isdir(path):
+        os.chmod(path, DIR_UMASK)
+    else:
+        os.chmod(path, UMASK)
+    os.chown(path, OWNER_ID, GRP_ID)
+    return
+
+def final_subfolder(stud_id):
+    # compute new folder name from stud_id
+    num = int(stud_id[1:])
+    num = num / 1000
+    return '%05d' % num
+
+def copy_file(file_src, file_dst):
+    print "COPY FILE: %s -> %s" % (file_src, file_dst)
+    shutil.copyfile(file_src, file_dst)
+    set_perms(file_dst)
+    return
+
+def create_path(path):
+    # create path with subdirs, if it does not exist (completely)
+    if os.path.exists(path):
+        return
+    parent = os.path.dirname(path)
+    if not os.path.exists(parent):
+        # create parent first
+        create_path(parent)
+    print "CREATE PATH ", path
+    os.mkdir(path)
+    set_perms(path)
+    return
+
+if not os.path.exists(SRC_DIR):
+    sys.exit("No source folder %s. Skipping." % SRC_DIR)
+
+if len(os.listdir(SRC_DIR)) == 0:
+    sys.exit("Empty source folder for %s. Skipping." % SRC_DIR)
+
+for stud_id in os.listdir(SRC_DIR):
+    subfolder_name = final_subfolder(stud_id)
+    dst_folder = os.path.join(DST_DIR, subfolder_name, stud_id)
+    if not os.path.exists(dst_folder):
+        #print "Destination folder does not exist: %s. Skipping" % (dst_folder)
+        #continue
+        create_path(dst_folder)
+
+    src_subfolder = os.path.join(SRC_DIR, stud_id)
+    for fingernumber in os.listdir(src_subfolder):
+        src_file = os.path.join(SRC_DIR, src_subfolder, fingernumber)
+        dst_file = os.path.join(DST_DIR, subfolder_name, stud_id,
+            'finger%s_%s.fpm' % (fingernumber, stud_id))
+        if os.path.exists(dst_file):
+            print "Destination file exists: %s. Skipping" % (dst_file)
+            continue
+        copy_file(src_file, dst_file)
Index: main/waeup.kofa/branches/uli-py3/tools/reload_portraits.py
===================================================================
--- main/waeup.kofa/branches/uli-py3/tools/reload_portraits.py	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/tools/reload_portraits.py	(revision 16045)
@@ -0,0 +1,130 @@
+##
+## This script is called without parameters.
+##
+## It moves passport images from a reload folder to their right place in
+## the media folder.
+##
+## Configuration is done below.
+##
+##
+## Please note that for changing groups of files/directories, you
+## normally have to be root. This script is therefore normally run with
+## sudo.
+##
+
+## ######################################################################
+## CONFIGURATION
+##
+
+## The folder where all source docs can be found (can be any folder)
+#SRC_DIR = "/kofa/uniben/var/datacenter/media/students/reload"
+SRC_DIR = "/kofa/uniben_passport_reload"
+
+## The students folder in Kofa where files should go to
+DST_DIR = "/kofa/uniben/var/datacenter/media/students"
+
+
+## Permissions to be set on new files/dirs. Set OWNER and/or GROUP to
+## None to leave them unchanged after creation.
+UMASK = 0664
+OWNER = 'kofa'
+GROUP = 'kofa'
+LETTER = 'B'
+
+##
+## CONFIGURATION END
+## ######################################################################
+
+import csv
+import os
+import shutil
+import sys
+from grp import getgrnam
+from pwd import getpwnam
+
+OWNER_ID = OWNER and getpwnam(OWNER)[2] or -1
+GRP_ID = GROUP and getgrnam(GROUP)[2] or -1
+DIR_UMASK = UMASK | 0111  # directories need extra x-permission
+
+def set_perms(path):
+    # set permissions and ownership for path
+    if os.path.isdir(path):
+        os.chmod(path, DIR_UMASK)
+    else:
+        os.chmod(path, UMASK)
+    os.chown(path, OWNER_ID, GRP_ID)
+    return
+
+def new_and_old_id(filename):
+    try:
+        stud_id, ext = filename.split('.')
+    except:
+        return None, None
+    if ext != 'jpg':
+        return None, None
+    if len(stud_id) == 7:
+        return LETTER + stud_id, stud_id
+    if len(stud_id) == 8:
+        return stud_id, stud_id
+    return None, None
+
+def final_subfolder(stud_id):
+    # compute new folder name from stud_id
+    try:
+        num = int(stud_id[1:])
+        if len(stud_id) == 7:
+            # old stud_id
+            num = num / 10000 * 10
+        if len(stud_id) == 8:
+            # new stud_id
+            num = num / 1000
+    except ValueError:
+        # new old stud_ids
+        num = int(stud_id[2:])
+        num = num / 10000 * 10
+    return '%05d' % num
+
+def copy_file(file_src, file_dst):
+    print "COPY FILE: %s -> %s" % (file_src, file_dst)
+    shutil.copyfile(file_src, file_dst)
+    set_perms(file_dst)
+    return
+
+def create_path(path):
+    # create path with subdirs, if it does not exist (completely)
+    if os.path.exists(path):
+        return
+    parent = os.path.dirname(path)
+    if not os.path.exists(parent):
+        # create parent first
+        create_path(parent)
+    print "CREATE PATH ", path
+    os.mkdir(path)
+    set_perms(path)
+    return
+
+if not os.path.exists(SRC_DIR):
+    sys.exit("No source folder %s. Skipping." % SRC_DIR)
+
+if len(os.listdir(SRC_DIR)) == 0:
+    sys.exit("Empty source folder for %s. Skipping." % SRC_DIR)
+
+for filename in os.listdir(SRC_DIR):
+    new_stud_id, stud_id = new_and_old_id(filename)
+    if not new_stud_id:
+        print "Wrong filename: %s" % filename
+        continue
+    subfolder = final_subfolder(stud_id)
+    dst_folder = os.path.join(DST_DIR, subfolder, new_stud_id)
+    if not os.path.exists(dst_folder):
+        #print "Destination folder does not exist: %s. Skipping" % (dst_folder)
+        #continue
+        create_path(dst_folder)
+    dst_file = os.path.join(DST_DIR, subfolder, new_stud_id,
+        'passport_%s.jpg' % new_stud_id)
+    if os.path.exists(dst_file):
+        print "Destination file exists: %s. Skipping" % (dst_file)
+        continue
+    file_src = os.path.join(SRC_DIR, filename)
+    file_dst = os.path.join(dst_file)
+    copy_file(file_src, file_dst)
Index: main/waeup.kofa/branches/uli-py3/versions.cfg
===================================================================
--- main/waeup.kofa/branches/uli-py3/versions.cfg	(revision 16045)
+++ main/waeup.kofa/branches/uli-py3/versions.cfg	(revision 16045)
@@ -0,0 +1,157 @@
+# Versions of packages used by waeup.kofa
+[buildout]
+# default set of versions we use.
+#extends= http://grok.zope.org/releaseinfo/grok-1.1.cfg
+extends = grok-1.1.cfg
+
+versions = versions
+
+[versions]
+alabaster = 0.7.7
+Babel = 1.3
+Beaker = 1.6.4
+collective.recipe.sphinxbuilder = 0.8.2
+collective.recipe.template = 2.1
+coverage = 3.6
+diazo = 1.0.6
+distribute = 0.6.49
+docutils = 0.11
+dolmen.beaker = 0.1
+experimental.cssselect = 0.3
+grok = 1.1.1
+grokcore.view = 1.13.5
+grokui.admin = 0.6.4
+grokui.base = 0.2.2
+horae.sphinx.grok = 1.0a1
+hurry.file = 1.2.1
+hurry.jquery = 1.4.3.1
+hurry.jqueryui = 1.8.5.1
+hurry.query = 1.1.1
+hurry.resource = 0.10
+hurry.workflow = 0.11
+hurry.zoperesource = 0.6
+Jinja2 = 2.3.1
+lovely.recipe = 1.0.0
+lxml = 3.3.6
+martian = 0.11.3
+mechanize = 0.2.5
+megrok.layout = 1.0.2
+megrok.menu = 0.4
+mock = 1.0.1
+mr.developer = 1.27
+Paste = 1.7.5.1
+PasteDeploy = 1.3.4
+PasteScript = 1.7.5
+Pillow = 2.0.0
+plone.recipe.command = 1.1
+psutil = 2.1.3
+py = 1.0.2
+pycryptopp = 0.6.0.1206569328141510525648634803928199668821045408958
+Pygments = 2.0.2
+pyprof2calltree = 1.1.1
+reportlab = 2.5
+repoze.profile = 2.0
+repoze.sphinx.autointerface = 0.7.1
+repoze.xmliter = 0.5
+RestrictedPython = 3.5.2
+rwproperty = 1.0
+setuptools = 19.2
+six = 1.10.0
+snowballstemmer = 1.2.1
+Sphinx = 1.3.5
+sphinx-rtd-theme = 0.1.9
+transaction = 1.1.1
+Twisted = 13.0.0
+ulif.loghandlers = 0.1.1
+unicodecsv = 0.9.4
+unittest2 = 0.5.1
+uuid = 1.30
+WebOb = 1.3.1
+z3c.autoinclude = 0.3.6
+# z3c.coverage 2.0 is out but does not cope with non-ASCII in sources
+z3c.coverage = 1.3.1
+z3c.recipe.staticlxml = 0.10
+z3c.testsetup = 0.6.1
+zc.async = 1.5.4
+zc.buildout = 2.5.0
+zc.catalog = 1.4.5
+zc.dict = 1.2.1
+zc.lockfile = 1.0.2
+zc.monitor = 0.3.1
+zc.ngi = 2.0.1
+zc.queue = 1.3
+zc.recipe.cmmi = 1.3.6
+zc.recipe.egg = 2.0.3
+zc.recipe.testrunner = 2.0.0
+zc.resourcelibrary = 1.3.4
+zc.twist = 1.3.1
+zc.z3monitor = 0.8.0
+zc.zodbrecipes = 0.6.2
+zdaemon = 2.0.7
+ZODB3 = 3.10.5
+zope.app.apidoc = 3.7.5
+zope.app.applicationcontrol = 3.5.10
+zope.app.basicskin = 3.5.1
+zope.app.dav = 3.5.3
+zope.app.error = 3.5.3
+zope.app.exception = 3.6.3
+zope.app.folder = 3.5.2
+zope.app.form = 4.0.2
+zope.app.i18n = 3.6.4
+zope.app.interface = 3.5.2
+zope.app.keyreference = 3.6.1
+zope.app.locales = 3.6.2
+zope.app.localpermission = 3.7.2
+zope.app.principalannotation = 3.7.1
+zope.app.publisher = 3.10.2
+zope.app.rotterdam = 3.5.3
+zope.app.session = 3.6.2
+zope.app.testing = 3.9.0
+zope.app.zcmlfiles = 3.7.1
+zope.app.zopeappgenerations = 3.5.1
+zope.authentication = 3.7.1
+zope.bforest = 1.2
+zope.browsermenu = 3.9.1
+zope.browserresource = 3.10.3
+zope.cachedescriptors = 3.5.1
+zope.catalog = 3.8.2
+zope.component = 3.9.5
+zope.componentvocabulary = 1.0.1
+zope.configuration = 3.7.4
+zope.container = 3.11.2
+zope.contenttype = 3.5.5
+zope.datetime = 3.4.1
+zope.deferredimport = 3.5.3
+zope.deprecation = 3.4.1
+zope.documenttemplate = 3.4.3
+zope.dublincore = 3.6.3
+zope.error = 3.7.4
+zope.errorview = 0.11
+zope.filerepresentation = 3.6.1
+zope.formlib = 4.0.6
+zope.i18n = 3.7.4
+zope.i18nmessageid = 3.5.3
+zope.index = 3.6.4
+zope.interface = 3.6.7
+zope.keyreference = 3.6.4
+zope.lifecycleevent = 3.6.2
+zope.location = 3.9.1
+zope.pagetemplate = 3.5.2
+zope.password = 3.6.1
+zope.pluggableauth = 1.0.3
+zope.principalannotation = 3.6.1
+zope.principalregistry = 3.7.1
+zope.publisher = 3.12.6
+zope.schema = 3.8.1
+zope.security = 3.7.4
+zope.sendmail = 3.7.5
+zope.server = 3.6.3
+zope.session = 3.9.5
+zope.site = 3.9.2
+zope.tales = 3.5.3
+zope.testbrowser = 4.0.4
+zope.testing = 3.10.3
+zope.testrunner = 4.3.3
+zope.traversing = 3.12.1
+zope.viewlet=3.7.2
+zope.xmlpickle = 3.4.0
